213 lines
9.5 KiB
Python
213 lines
9.5 KiB
Python
# 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,
|
|
}
|