194 lines
8.1 KiB
Python
194 lines
8.1 KiB
Python
# Copyright 2015-2024 Akretion France (https://www.akretion.com)
|
|
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
from datetime import timedelta
|
|
import logging
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.osv import expression
|
|
from odoo.tools import float_is_zero
|
|
from odoo.tools.misc import format_date
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = 'account.move'
|
|
|
|
invoice_date_due = fields.Date(tracking=True)
|
|
invoice_payment_term_id = fields.Many2one(tracking=True)
|
|
journal_id = fields.Many2one(tracking=True)
|
|
fiscal_position_id = fields.Many2one(tracking=True)
|
|
amount_total = fields.Monetary(tracking=True)
|
|
# for invoice report
|
|
has_discount = fields.Boolean(compute='_compute_has_discount')
|
|
# has_attachment is useful for those who use attachment to archive
|
|
# supplier invoices. It allows them to find supplier invoices
|
|
# that don't have any attachment
|
|
has_attachment = fields.Boolean(
|
|
compute='_compute_has_attachment', search='_search_has_attachment')
|
|
sale_dates = fields.Char(
|
|
compute="_compute_sales_dates",
|
|
help="This information appear on invoice qweb report "
|
|
"(you may use it for your own report)")
|
|
# The native "blocked" field (bool) on account.move.line has been removed in v18
|
|
# blocked = fields.Boolean(
|
|
# compute="_compute_blocked",
|
|
# inverse="_inverse_blocked",
|
|
# store=True,
|
|
# string="Dispute",
|
|
# tracking=True,
|
|
# )
|
|
# Field search_account_id is just for search view
|
|
search_account_id = fields.Many2one(related='line_ids.account_id')
|
|
|
|
# @api.depends("line_ids", "line_ids.blocked")
|
|
# def _compute_blocked(self):
|
|
# for move in self:
|
|
# move.blocked = any(
|
|
# [
|
|
# l.blocked
|
|
# for l in move.line_ids
|
|
# if l.account_id.account_type in ("liability_payable", "asset_receivable")
|
|
# ]
|
|
# )
|
|
|
|
# def _inverse_blocked(self):
|
|
# for move in self:
|
|
# for line in move.line_ids.filtered(
|
|
# lambda l: l.account_id.account_type in ("liability_payable", "asset_receivable")
|
|
# ):
|
|
# line.blocked = move.blocked
|
|
|
|
def _compute_has_discount(self):
|
|
prec = self.env['decimal.precision'].precision_get('Discount')
|
|
for inv in self:
|
|
has_discount = False
|
|
for line in inv.invoice_line_ids:
|
|
if line.display_type == 'product' and not float_is_zero(line.discount, precision_digits=prec):
|
|
has_discount = True
|
|
break
|
|
inv.has_discount = has_discount
|
|
|
|
def _compute_has_attachment(self):
|
|
iao = self.env['ir.attachment']
|
|
for move in self:
|
|
if iao.search_count([
|
|
('res_model', '=', 'account.move'),
|
|
('res_id', '=', move.id),
|
|
('type', '=', 'binary'),
|
|
('company_id', '=', move.company_id.id)]):
|
|
move.has_attachment = True
|
|
else:
|
|
move.has_attachment = False
|
|
|
|
def _search_has_attachment(self, operator, value):
|
|
att_inv_ids = {}
|
|
if operator == '=':
|
|
search_res = self.env['ir.attachment'].search_read([
|
|
('res_model', '=', 'account.move'),
|
|
('type', '=', 'binary'),
|
|
('res_id', '!=', False)], ['res_id'])
|
|
for att in search_res:
|
|
att_inv_ids[att['res_id']] = True
|
|
res = [('id', value and 'in' or 'not in', list(att_inv_ids))]
|
|
return res
|
|
|
|
def delete_lines_qty_zero(self):
|
|
lines = self.env['account.move.line'].search([
|
|
('display_type', '=', 'product'),
|
|
('move_id', 'in', self.ids),
|
|
('quantity', '=', 0)])
|
|
lines.unlink()
|
|
return True
|
|
|
|
# for report
|
|
def py3o_lines_layout(self):
|
|
self.ensure_one()
|
|
res = []
|
|
has_sections = False
|
|
subtotal = 0.0
|
|
sign = self.move_type == 'out_refund' and -1 or 1
|
|
# Warning: the order of invoice line is forced in the view
|
|
# <tree editable="bottom" default_order="sequence, date desc, move_name desc, id"
|
|
# it's not the same as the _order in the class AccountMoveLine
|
|
lines = self.env['account.move.line'].search([('display_type', 'in', ('product', 'line_section', 'line_note')), ('move_id', '=', self.id)], order="sequence, date desc, move_name desc, id")
|
|
for line in lines:
|
|
if line.display_type == 'line_section':
|
|
# insert line
|
|
if has_sections:
|
|
res.append({'subtotal': subtotal})
|
|
subtotal = 0.0 # reset counter
|
|
has_sections = True
|
|
else:
|
|
if line.display_type == 'product':
|
|
subtotal += line.price_subtotal * sign
|
|
res.append({'line': line})
|
|
if has_sections: # insert last subtotal line
|
|
res.append({'subtotal': subtotal})
|
|
# res:
|
|
# [
|
|
# {'line': account_invoice_line(1) with display_type=='line_section'},
|
|
# {'line': account_invoice_line(2) with display_type=='product'},
|
|
# {'line': account_invoice_line(3) with display_type=='product'},
|
|
# {'line': account_invoice_line(4) with display_type=='line_note'},
|
|
# {'subtotal': 8932.23},
|
|
# ]
|
|
return res
|
|
|
|
def _compute_sales_dates(self):
|
|
""" French law requires to set sale order dates into invoice
|
|
returned string: "sale1 (date1), sale2 (date2) ..."
|
|
"""
|
|
for move in self:
|
|
sales = move.invoice_line_ids.mapped(
|
|
'sale_line_ids').mapped('order_id')
|
|
dates = ["%s (%s)" % (
|
|
x.name, format_date(move.env, x.date_order))
|
|
for x in sales]
|
|
move.sale_dates = ", ".join(dates)
|
|
|
|
# allow to manually create moves not only in general journals,
|
|
# but also in cash journal and check journals (= bank journals not linked to a bank account)
|
|
@api.depends('company_id', 'invoice_filter_type_domain')
|
|
def _compute_suitable_journal_ids(self):
|
|
for move in self:
|
|
if move.invoice_filter_type_domain:
|
|
super(AccountMove, move)._compute_suitable_journal_ids()
|
|
else:
|
|
company_id = move.company_id.id or self.env.company.id
|
|
domain = expression.AND([
|
|
[('company_id', '=', company_id)],
|
|
expression.OR([
|
|
[('type', 'in', ('general', 'cash'))],
|
|
[('type', '=', 'bank'), ('bank_account_id', '=', False)]
|
|
])
|
|
])
|
|
move.suitable_journal_ids = self.env['account.journal'].search(domain)
|
|
|
|
# There is no more attachment by default on invoice reports...
|
|
# TODO check what's the editor strategy on this
|
|
# def button_draft(self):
|
|
# Delete attached pdf invoice
|
|
# for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
|
|
# for report_xmlid in ('account.account_invoices', 'account.account_invoices_without_payment'):
|
|
# report = self.env.ref(report_xmlid)
|
|
# attach = report.retrieve_attachment(move)
|
|
# if attach:
|
|
# attach.unlink()
|
|
# super().button_draft()
|
|
|
|
def _get_accounting_date(self, invoice_date, has_tax, lock_dates=None):
|
|
# On vendor bills/refunds, we want date = invoice_date unless
|
|
# we have a company tax_lock_date and the invoice has taxes
|
|
# and invoice_date <= tax_lock_date
|
|
date = super()._get_accounting_date(invoice_date, has_tax, lock_dates=lock_dates)
|
|
lock_dates = lock_dates or self._get_violated_lock_dates(invoice_date, has_tax)
|
|
if self.is_purchase_document(include_receipts=True) and invoice_date:
|
|
if lock_dates:
|
|
date = max([entry[0] for entry in lock_dates]) + timedelta(1)
|
|
else:
|
|
date = invoice_date
|
|
return date
|