# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, fields, models from odoo.exceptions import UserError class AccountMove(models.Model): _inherit = 'account.move' l10n_it_edi_doi_date = fields.Date( string="Date on which Declaration of Intent is applied", compute='_compute_l10n_it_edi_doi_date', ) l10n_it_edi_doi_use = fields.Boolean( string="Use Declaration of Intent", compute='_compute_l10n_it_edi_doi_use', ) l10n_it_edi_doi_id = fields.Many2one( string="Declaration of Intent", compute='_compute_l10n_it_edi_doi_id', store=True, readonly=False, precompute=True, comodel_name='l10n_it_edi_doi.declaration_of_intent', ) l10n_it_edi_doi_amount = fields.Monetary( string='Declaration of Intent Amount', compute='_compute_l10n_it_edi_doi_amount', store=True, readonly=True, help="Total amount of sales under the Declaration of Intent of this document", ) l10n_it_edi_doi_warning = fields.Text( string="Declaration of Intent Threshold Warning", compute='_compute_l10n_it_edi_doi_warning', ) @api.depends('invoice_date') def _compute_l10n_it_edi_doi_date(self): for move in self: move.l10n_it_edi_doi_date = move.invoice_date or fields.Date.context_today(self) @api.depends('l10n_it_edi_doi_id', 'country_code', 'move_type') def _compute_l10n_it_edi_doi_use(self): sale_types = self.env['account.move'].get_sale_types() for move in self: move.l10n_it_edi_doi_use = ( move.l10n_it_edi_doi_id or (move.country_code == "IT" and move.move_type in sale_types) ) @api.depends('company_id', 'partner_id.commercial_partner_id', 'l10n_it_edi_doi_date', 'currency_id') def _compute_l10n_it_edi_doi_id(self): for move in self: if not move.l10n_it_edi_doi_use or move.state != 'draft' and not move.l10n_it_edi_doi_id: move.l10n_it_edi_doi_id = False continue partner = move.partner_id.commercial_partner_id # Avoid a query or changing a manually set declaration of intent # (if the declaration is still valid). validity_warnings = move.l10n_it_edi_doi_id._get_validity_warnings( move.company_id, partner, move.currency_id, move.l10n_it_edi_doi_date ) if move.l10n_it_edi_doi_id and not validity_warnings: continue declaration = self.env['l10n_it_edi_doi.declaration_of_intent']\ ._fetch_valid_declaration_of_intent(move.company_id, partner, move.currency_id, move.l10n_it_edi_doi_date) move.l10n_it_edi_doi_id = declaration @api.depends('l10n_it_edi_doi_id', 'tax_totals', 'move_type') def _compute_l10n_it_edi_doi_amount(self): """ Consider all the lines in self that belong to declaration of intent `declaration` and have the special declaration of intent tax applied. This function computes the signed sum of the price_total of all those lines (the tax amount of the lines is always 0). The direction_sign determines the sign: 1 (-1) for inbound (outbound) types. """ for move in self: tax = move.company_id.l10n_it_edi_doi_tax_id if not tax or not move.l10n_it_edi_doi_id: move.l10n_it_edi_doi_amount = 0 continue declaration_lines = move.invoice_line_ids.filtered( # The declaration tax cannot be used with other taxes on a single line # (checked in `_post`) lambda line: line.tax_ids.ids == tax.ids ) move.l10n_it_edi_doi_amount = sum(declaration_lines.mapped('price_total')) * -move.direction_sign @api.depends('l10n_it_edi_doi_id', 'l10n_it_edi_doi_amount', 'state') def _compute_l10n_it_edi_doi_warning(self): for move in self: move.l10n_it_edi_doi_warning = '' declaration = move.l10n_it_edi_doi_id show_warning = ( declaration and move.is_sale_document(include_receipts=False) and move.state != 'cancel' ) if not show_warning: continue declaration_invoiced = declaration.invoiced declaration_not_yet_invoiced = declaration.not_yet_invoiced if move.state != 'posted': # exactly the 'posted' invoices are included in declaration.invoiced # Here we replicate what would happen when posting the invoice. # Note: lines manually added to a move linked to a sales order are not added to the sales order declaration_invoiced += move.l10n_it_edi_doi_amount additional_invoiced_qty = {} linked_orders = self.env['sale.order'] for invoice_line in move.invoice_line_ids: for sale_line in invoice_line.sale_line_ids: order = sale_line.order_id if order.l10n_it_edi_doi_id == declaration: linked_orders |= order qty_invoiced = invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, sale_line.product_uom) * -move.direction_sign sale_line_id = sale_line.ids[0] # do not just use `id` in case of NewId additional_invoiced_qty[sale_line_id] = additional_invoiced_qty.get(sale_line_id, 0) + qty_invoiced for order in linked_orders: not_yet_invoiced = order.l10n_it_edi_doi_not_yet_invoiced not_yet_invoiced_after_posting = order._l10n_it_edi_doi_get_amount_not_yet_invoiced( declaration, additional_invoiced_qty=additional_invoiced_qty, ) declaration_not_yet_invoiced -= not_yet_invoiced - not_yet_invoiced_after_posting validity_warnings = declaration._get_validity_warnings( move.company_id, move.commercial_partner_id, move.currency_id, move.l10n_it_edi_doi_date, invoiced_amount=declaration_invoiced, ) threshold_warning = declaration._build_threshold_warning_message(declaration_invoiced, declaration_not_yet_invoiced) move.l10n_it_edi_doi_warning = '{}\n\n{}'.format('\n'.join(validity_warnings), threshold_warning).strip() @api.depends('l10n_it_edi_doi_id') def _compute_fiscal_position_id(self): super()._compute_fiscal_position_id() for move in self: declaration_fiscal_position = move.company_id.l10n_it_edi_doi_fiscal_position_id if declaration_fiscal_position and move.l10n_it_edi_doi_id: move.fiscal_position_id = declaration_fiscal_position def copy_data(self, default=None): data_list = super().copy_data(default) for move, data in zip(self, data_list): date = fields.Date.context_today(self) validity_warnings = move.l10n_it_edi_doi_id._get_validity_warnings( move.company_id, move.commercial_partner_id, move.currency_id, date, only_blocking=True, ) if validity_warnings: del data['l10n_it_edi_doi_id'] del data['fiscal_position_id'] return data_list @api.constrains('l10n_it_edi_doi_id') def _check_l10n_it_edi_doi_id(self): for move in self: validity_errors = move.l10n_it_edi_doi_id._get_validity_errors( move.company_id, move.partner_id.commercial_partner_id, move.currency_id ) if validity_errors: raise UserError('\n'.join(validity_errors)) def _post(self, soft=True): errors = [] for move in self: declaration = move.l10n_it_edi_doi_id if declaration: validity_warnings = declaration._get_validity_warnings( move.company_id, move.commercial_partner_id, move.currency_id, move.l10n_it_edi_doi_date, invoiced_amount=move.l10n_it_edi_doi_amount, only_blocking=True ) errors.extend(validity_warnings) declaration_of_intent_tax = move.company_id.l10n_it_edi_doi_tax_id if not declaration_of_intent_tax: continue declaration_lines = move.invoice_line_ids.filtered( lambda line: declaration_of_intent_tax in line.tax_ids ) if declaration_lines and not declaration: errors.append(_('Given the tax %s is applied, there should be a Declaration of Intent selected.', declaration_of_intent_tax.name)) if any(line.tax_ids != declaration_of_intent_tax for line in declaration_lines): errors.append(_('A line using tax %s should not contain any other taxes', declaration_of_intent_tax.name)) if errors: raise UserError('\n'.join(errors)) return super()._post(soft) def action_open_declaration_of_intent(self): self.ensure_one() return { 'name': _("Declaration of Intent for %s", self.display_name), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'l10n_it_edi_doi.declaration_of_intent', 'res_id': self.l10n_it_edi_doi_id.id, }