Odoo18-Base/addons/l10n_it_edi_doi/models/declaration_of_intent.py
2025-01-06 10:57:38 +07:00

314 lines
13 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
from odoo.tools.misc import formatLang
class L10nItDeclarationOfIntent(models.Model):
_name = "l10n_it_edi_doi.declaration_of_intent"
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin']
_description = "Declaration of Intent"
_order = 'protocol_number_part1, protocol_number_part2'
state = fields.Selection([
('draft', 'Draft'),
('active', 'Active'),
('revoked', 'Revoked'),
('terminated', 'Terminated'),
],
string="State",
tracking=True,
default='draft',
required=True,
readonly=True,
help="The state of this Declaration of Intent. \n"
"- 'Draft' means that the Declaration of Intent still needs to be confirmed before being usable. \n"
"- 'Active' means that the Declaration of Intent is usable. \n"
"- 'Terminated' designates that the Declaration of Intent has been marked as not to use anymore without invalidating usages of it. \n"
"- 'Revoked' means the Declaration of Intent should not have been used. You will probably need to revert previous usages of it, if any.\n")
company_id = fields.Many2one(
comodel_name='res.company',
string='Company',
index=True,
required=True,
default=lambda self: self.env.company._accessible_branches()[:1],
)
partner_id = fields.Many2one(
comodel_name='res.partner',
string='Partner',
index=True,
required=True,
domain="['|', ('is_company', '=', True), ('parent_id', '=', False)]",
)
currency_id = fields.Many2one(
comodel_name='res.currency',
string='Currency',
default=lambda self: self.env.ref('base.EUR', raise_if_not_found=False).id,
required=True,
readonly=True,
)
issue_date = fields.Date(
string='Date of Issue',
required=True,
copy=False,
default=fields.Date.context_today,
help="Date on which the Declaration of Intent was issued",
)
start_date = fields.Date(
string='Start Date',
required=True,
copy=False,
help="First date on which the Declaration of Intent is valid",
)
end_date = fields.Date(
string='End Date',
required=True,
copy=False,
help="Last date on which the Declaration of Intent is valid",
)
threshold = fields.Monetary(
string='Threshold',
required=True,
help="Total amount of allowed sales without VAT under this Declaration of Intent",
)
invoiced = fields.Monetary(
string='Invoiced',
compute='_compute_invoiced',
store=True,
readonly=True,
help="Total amount of sales under this Declaration of Intent",
)
not_yet_invoiced = fields.Monetary(
string='Not Yet Invoiced',
compute='_compute_not_yet_invoiced',
store=True,
readonly=True,
help="Total amount of planned sales under this Declaration of Intent (i.e. current quotation and sales orders) that can still be invoiced",
)
remaining = fields.Monetary(
string='Remaining',
compute='_compute_remaining',
store=True,
readonly=True,
help="Remaining amount after deduction of the Invoiced and Not Yet Invoiced amounts.",
)
protocol_number_part1 = fields.Char(
string='Protocol 1',
required=True,
readonly=False,
copy=False,
)
protocol_number_part2 = fields.Char(
string='Protocol 2',
required=True,
readonly=False,
copy=False,
)
invoice_ids = fields.One2many(
'account.move',
'l10n_it_edi_doi_id',
string="Invoices / Refunds",
copy=False,
readonly=True,
)
sale_order_ids = fields.One2many(
'sale.order',
'l10n_it_edi_doi_id',
string="Sales Orders / Quotations",
copy=False,
readonly=True,
)
_sql_constraints = [
('protocol_number_unique',
'unique(protocol_number_part1, protocol_number_part2)',
"The Protocol Number of a Declaration of Intent must be unique! Please choose another one."),
('threshold_positive',
'CHECK(threshold > 0)',
"The Threshold of a Declaration of Intent must be positive."),
]
@api.depends('protocol_number_part1', 'protocol_number_part2')
def _compute_display_name(self):
for record in self:
record.display_name = f"{record.protocol_number_part1}-{record.protocol_number_part2}"
@api.depends('invoice_ids', 'invoice_ids.state', 'invoice_ids.l10n_it_edi_doi_amount')
def _compute_invoiced(self):
for declaration in self:
relevant_invoices = declaration.invoice_ids.filtered(
lambda invoice: invoice.state == 'posted'
)
declaration.invoiced = sum(relevant_invoices.mapped('l10n_it_edi_doi_amount'))
@api.depends('sale_order_ids', 'sale_order_ids.state', 'sale_order_ids.l10n_it_edi_doi_not_yet_invoiced')
def _compute_not_yet_invoiced(self):
for declaration in self:
relevant_orders = declaration.sale_order_ids.filtered(
lambda order: order.state == 'sale'
)
declaration.not_yet_invoiced = sum(relevant_orders.mapped('l10n_it_edi_doi_not_yet_invoiced'))
@api.depends('threshold', 'not_yet_invoiced', 'invoiced')
def _compute_remaining(self):
for record in self:
record.remaining = record.threshold - record.invoiced - record.not_yet_invoiced
def _build_threshold_warning_message(self, invoiced, not_yet_invoiced):
"""
Build a warning message that will be displayed in a yellow banner on top of a document
if the `remaining` of the Declaration of Intent is less than 0 when including the document
or the Declaration of Intent is revoked
:param float invoiced: The `declaration.invoiced` amount when including the document.
:param float not_yet_invoiced: The `declaration.not_yet_invoiced` amount when including the document.
:return str: The warning message to be shown.
"""
self.ensure_one()
updated_remaining = self.threshold - invoiced - not_yet_invoiced
if self.currency_id.compare_amounts(updated_remaining, 0) >= 0:
return ''
return _(
'Pay attention, the threshold of your Declaration of Intent %(name)s of %(threshold)s is exceeded by %(exceeded)s, this document included.\n'
'Invoiced: %(invoiced)s; Not Yet Invoiced: %(not_yet_invoiced)s',
name=self.display_name,
threshold=formatLang(self.env, self.threshold, currency_obj=self.currency_id),
exceeded=formatLang(self.env, - updated_remaining, currency_obj=self.currency_id),
invoiced=formatLang(self.env, invoiced, currency_obj=self.currency_id),
not_yet_invoiced=formatLang(self.env, not_yet_invoiced, currency_obj=self.currency_id),
)
def _get_validity_errors(self, company, partner, currency):
"""
Check whether all declarations of intent in self are valid for the specified `company`, `partner`, `date` and `currency'.
Violating these constraints leads to errors in the feature. They should not be ignored.
Return all errors as a list of strings.
"""
errors = []
for declaration in self:
if not company or declaration.company_id != company:
errors.append(_("The Declaration of Intent belongs to company %(declaration_company)s, not %(company)s.",
declaration_company=declaration.company_id.name, company=company.name))
if not currency or declaration.currency_id != currency:
errors.append(_("The Declaration of Intent uses currency %(declaration_currency)s, not %(currency)s.",
declaration_currency=declaration.currency_id.name, currency=currency.name))
if not partner or declaration.partner_id != partner.commercial_partner_id:
errors.append(_("The Declaration of Intent belongs to partner %(declaration_partner)s, not %(partner)s.",
declaration_partner=declaration.partner_id.name, partner=partner.commercial_partner_id.name))
return errors
def _get_validity_warnings(self, company, partner, currency, date, invoiced_amount=0, only_blocking=False, sales_order=False):
"""
Check whether all declarations of intent in self are valid for the specified `company`, `partner`, `date` and `currency'.
The checks for `date` and state of the declaration (except draft) are not considered blocking in case `invoiced_amount` is not positive.
All other checks are considered blocking (prevent posting).
Includes all checks from `_get_validity_errors`.
The checks are different for invoices and sales orders (toggled via kwarg `sales_order`).
I.e. we do not care about the date for sales orders.
Return all errors as a list of strings.
"""
errors = []
for declaration in self:
errors.extend(declaration._get_validity_errors(company, partner, currency))
if declaration.state == 'draft':
errors.append(_("The Declaration of Intent is in draft."))
if declaration.currency_id.compare_amounts(invoiced_amount, 0) > 0 or not only_blocking:
if declaration.state != 'active':
errors.append(_("The Declaration of Intent must be active."))
if not sales_order and (not date or declaration.start_date > date or declaration.end_date < date):
errors.append(_("The Declaration of Intent is valid from %(start_date)s to %(end_date)s, not on %(date)s.",
start_date=declaration.start_date, end_date=declaration.end_date, date=date))
return errors
@api.model
def _fetch_valid_declaration_of_intent(self, company, partner, currency, date):
"""
Fetch a declaration of intent that is valid for the specified `company`, `partner`, `date` and `currency`
and has not reached the threshold yet.
"""
return self.search([
('state', '=', 'active'),
('company_id', '=', company.id),
('currency_id', '=', currency.id),
('partner_id', '=', partner.commercial_partner_id.id),
('start_date', '<=', date),
('end_date', '>=', date),
('remaining', '>', 0),
], limit=1)
@api.ondelete(at_uninstall=False)
def _unlink_except_linked_to_document(self):
if self.invoice_ids or self.sale_order_ids:
raise UserError(_('You cannot delete Declarations of Intents that are already used on at least one Invoice or Sales Order.'))
def action_validate(self):
""" Move a 'draft' Declaration of Intent to 'active'."""
for record in self:
if record.state == 'draft':
record.state = 'active'
def action_reset_to_draft(self):
""" Resets an 'active' Declaration of Intent back to 'draft'."""
for record in self:
if record.state == 'active':
record.state = 'draft'
def action_reactivate(self):
""" Resets a not 'active' Declaration of Intent back to 'active'."""
for record in self:
if record.state != 'active':
record.state = 'active'
def action_revoke(self):
""" Called by the 'revoke' button of the form view."""
for record in self:
record.state = 'revoked'
def action_terminate(self):
""" Called by the 'terminated' button of the form view."""
for record in self:
if record.state != 'revoked':
record.state = 'terminated'
def action_open_sale_order_ids(self):
self.ensure_one()
return {
'name': _("Sales Orders using Declaration of Intent %s", self.display_name),
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'domain': [('id', 'in', self.sale_order_ids.ids)],
'views': [(self.env.ref('l10n_it_edi_doi.view_quotation_tree').id, 'list'), (False, 'form')],
'search_view_id': [self.env.ref('sale.sale_order_view_search_inherit_quotation').id],
'context': {
'search_default_sales': 1,
},
}
def action_open_invoice_ids(self):
self.ensure_one()
return {
'name': _("Invoices using Declaration of Intent %s", self.display_name),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'domain': [('id', 'in', self.invoice_ids.ids)],
'views': [(self.env.ref('l10n_it_edi_doi.view_move_tree').id, 'list'), (False, 'form')],
'search_view_id': [self.env.ref('account.view_account_invoice_filter').id],
'context': {
'search_default_posted': 1,
},
}