314 lines
13 KiB
Python
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,
|
|
},
|
|
}
|