From 0b5783b6ead2a1dbba112bcd15446ef67d8bf42d Mon Sep 17 00:00:00 2001 From: Alessandro Uffreduzzi Date: Tue, 17 Dec 2024 15:35:03 +0100 Subject: [PATCH] DO NOT MERGE: sale_commission_partial_settlement: major refactor --- .../__manifest__.py | 5 +- .../migrations/14.0.1.2.1/post-migrate.py | 5 +- .../models/__init__.py | 1 + .../models/account_invoice_line_agent.py | 115 ++++++++---------- .../account_invoice_line_agent_partial.py | 107 ++++++++++++++-- .../models/account_partial_reconcile.py | 36 +++--- .../models/sale_commission_settlement.py | 6 +- .../models/sale_commission_settlement_line.py | 23 +++- ...sale_commission_settlement_line_partial.py | 65 ++++++++++ .../security/ir.model.access.csv | 2 + .../tests/test_partial_settlement.py | 12 +- ...count_invoice_line_agent_partial_views.xml | 42 +++++++ .../account_invoice_line_agent_views.xml | 52 ++++++++ ...mmission_settlement_line_partial_views.xml | 48 ++++++++ .../wizard/wizard_settle.py | 100 +++++++-------- 15 files changed, 459 insertions(+), 160 deletions(-) create mode 100644 sale_commission_partial_settlement/models/sale_commission_settlement_line_partial.py create mode 100644 sale_commission_partial_settlement/views/account_invoice_line_agent_partial_views.xml create mode 100644 sale_commission_partial_settlement/views/account_invoice_line_agent_views.xml create mode 100644 sale_commission_partial_settlement/views/sale_commission_settlement_line_partial_views.xml diff --git a/sale_commission_partial_settlement/__manifest__.py b/sale_commission_partial_settlement/__manifest__.py index 0f6d85fda..2ba7d9cc1 100644 --- a/sale_commission_partial_settlement/__manifest__.py +++ b/sale_commission_partial_settlement/__manifest__.py @@ -1,7 +1,7 @@ # Copyright 2023 Nextev { "name": "Sales commissions based on paid amount", - "version": "14.0.1.2.1", + "version": "14.0.2.0.0", "author": "Nextev Srl," "Ooops," "Odoo Community Association (OCA)", "maintainers": ["aleuffre", "renda-dev", "PicchiSeba"], "category": "Sales Management", @@ -10,7 +10,10 @@ "website": "https://github.com/OCA/commission", "data": [ "security/ir.model.access.csv", + "views/account_invoice_line_agent_views.xml", + "views/account_invoice_line_agent_partial_views.xml", "views/res_config_settings_view.xml", + "views/sale_commission_settlement_line_partial_views.xml", "views/sale_commission_settlement_view.xml", "views/sale_commission_view.xml", ], diff --git a/sale_commission_partial_settlement/migrations/14.0.1.2.1/post-migrate.py b/sale_commission_partial_settlement/migrations/14.0.1.2.1/post-migrate.py index ed23e8407..6bcd625a4 100644 --- a/sale_commission_partial_settlement/migrations/14.0.1.2.1/post-migrate.py +++ b/sale_commission_partial_settlement/migrations/14.0.1.2.1/post-migrate.py @@ -5,8 +5,11 @@ def recompute_partial_commission_settled(env): """ Recompute field "partial_commission_settled" of model "account.partial.reconcile" + Removed in future versions of the module """ - env["account.partial.reconcile"].search([])._compute_partial_commission_settled() + partial_reconcile = env["account.partial.reconcile"] + if getattr(partial_reconcile, "_compute_partial_commission_settled", False): + partial_reconcile.search([])._compute_partial_commission_settled() @openupgrade.migrate() diff --git a/sale_commission_partial_settlement/models/__init__.py b/sale_commission_partial_settlement/models/__init__.py index da2661b51..562d39d4b 100644 --- a/sale_commission_partial_settlement/models/__init__.py +++ b/sale_commission_partial_settlement/models/__init__.py @@ -6,3 +6,4 @@ from . import sale_commission from . import sale_commission_settlement from . import sale_commission_settlement_line +from . import sale_commission_settlement_line_partial diff --git a/sale_commission_partial_settlement/models/account_invoice_line_agent.py b/sale_commission_partial_settlement/models/account_invoice_line_agent.py index c30b7ab62..c3b4f862d 100644 --- a/sale_commission_partial_settlement/models/account_invoice_line_agent.py +++ b/sale_commission_partial_settlement/models/account_invoice_line_agent.py @@ -1,13 +1,14 @@ # Copyright 2023 Nextev # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.tools.float_utils import float_compare class AccountInvoiceLineAgent(models.Model): _inherit = "account.invoice.line.agent" + payment_amount_type = fields.Selection(related="commission_id.payment_amount_type") partial_settled = fields.Monetary( string="Partial Commission Amount Settled", compute="_compute_partial_settled", @@ -15,22 +16,23 @@ class AccountInvoiceLineAgent(models.Model): ) is_fully_settled = fields.Boolean(compute="_compute_is_fully_settled", store=True) invoice_line_agent_partial_ids = fields.One2many( - "account.invoice.line.agent.partial", "invoice_line_agent_id" + "account.invoice.line.agent.partial", + "invoice_line_agent_id", + compute="_compute_invoice_line_agent_partial_ids", + store=True, + ) + commission_settlement_line_partial_ids = fields.One2many( + "sale.commission.settlement.line.partial", + compute="_compute_commission_settlement_line_partial_ids", ) @api.depends( - "invoice_line_agent_partial_ids.amount", - "invoice_line_agent_partial_ids.agent_line.settlement_id.state", + "invoice_line_agent_partial_ids.settled_amount", ) def _compute_partial_settled(self): for rec in self: rec.partial_settled = sum( - ailap.amount - for ailap in rec.invoice_line_agent_partial_ids - if any( - settlement.state != "cancel" - for settlement in ailap.mapped("agent_line.settlement_id") - ) + ailap.settled_amount for ailap in rec.invoice_line_agent_partial_ids ) @api.depends( @@ -50,62 +52,47 @@ def _compute_is_fully_settled(self): == 0 ) - def _partial_commissions(self, date_payment_to): + @api.depends( + "commission_id.payment_amount_type", + "object_id.move_id.move_type", + "object_id.move_id.line_ids.amount_residual", + ) + def _compute_invoice_line_agent_partial_ids(self): """ - This method iterates through agent invoice lines and calculates - partial commissions based on the payment amount. - If the partial payment amount is greater than the invoice line - amount, it fully settles the corresponding agent line. - Otherwise, it calculates the partial commission proportionally to - the amount paid, invoice amount and total commissions. + Create an account.invoice.line.agent.partial for each + payment term move line """ - partial_lines_to_settle = [] - partial_payment_remaining = {} - for line in self: - line_total_amount = line.amount - for ( - partial, - amount, - counterpart_line, - ) in line.invoice_id._get_reconciled_invoices_partials(): - if partial.partial_commission_settled: - continue - elif date_payment_to and date_payment_to < counterpart_line.date: - break - if partial.id in partial_payment_remaining: - payment_amount = partial_payment_remaining[partial.id][ - "remaining_amount" - ] - else: - payment_amount = amount - partial_payment_remaining[partial.id] = {"remaining_amount": amount} - if line.object_id.price_total <= payment_amount: - partial_lines_to_settle.append( - { - "invoice_line_agent_id": line.id, - "currency_id": line.currency_id.id, - "amount": line_total_amount, - "account_partial_reconcile_id": partial.id, - } + for rec in self: + ailap_model = rec.invoice_line_agent_partial_ids.browse() + if rec.commission_id.payment_amount_type != "paid": + rec.invoice_line_agent_partial_ids = False + continue + pay_term_lines = rec.object_id.move_id.line_ids.filtered( + lambda line: line.account_internal_type in ("receivable", "payable") + ) + forecast_lines = rec.invoice_line_agent_partial_ids.mapped("move_line_id") + for move_line in pay_term_lines: + if move_line not in forecast_lines: + ailap_model.create( + {"move_line_id": move_line.id, "invoice_line_agent_id": rec.id} ) - partial_payment_remaining[partial.id] = { - "remaining_amount": amount - line.object_id.price_total - } - break - paid_in_proportion = payment_amount / line.invoice_id.amount_total - partial_commission = ( - line.invoice_id.commission_total * paid_in_proportion - ) - partial_lines_to_settle.append( - { - "invoice_line_agent_id": line.id, - "currency_id": line.currency_id.id, - "amount": partial_commission, - "account_partial_reconcile_id": partial.id, - } - ) - partial_agent_lines = self.env["account.invoice.line.agent.partial"].create( - partial_lines_to_settle + def _compute_commission_settlement_line_partial_ids(self): + for rec in self: + rec.commission_settlement_line_partial_ids = ( + rec.invoice_line_agent_partial_ids.settlement_line_partial_ids + ) + + def action_see_partial_commissions(self): + view = self.env.ref( + "sale_commission_partial_settlement.account_invoice_line_agent_form_partial_only" ) - return partial_agent_lines + return { + "name": _("Partial Commissions"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": self._name, + "views": [(view.id, "form")], + "target": "new", + "res_id": self.id, + } diff --git a/sale_commission_partial_settlement/models/account_invoice_line_agent_partial.py b/sale_commission_partial_settlement/models/account_invoice_line_agent_partial.py index 181a7f8c9..98cb1df00 100644 --- a/sale_commission_partial_settlement/models/account_invoice_line_agent_partial.py +++ b/sale_commission_partial_settlement/models/account_invoice_line_agent_partial.py @@ -1,28 +1,115 @@ # Copyright 2023 Nextev # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class AccountInvoiceLineAgentPartial(models.Model): _name = "account.invoice.line.agent.partial" - _description = "Partial agent commissions" + _description = "Partial agent commissions. " + "Tracks the expected commissions." + move_line_id = fields.Many2one( + "account.move.line", + required=True, # TODO: migration? Probably cannot enforce + ondelete="cascade", + ) invoice_line_agent_id = fields.Many2one( "account.invoice.line.agent", required=True, ondelete="cascade" ) - # logically a One2one - agent_line = fields.Many2many( - comodel_name="sale.commission.settlement.line", - relation="settlement_agent_line_partial_rel", - column1="agent_line_partial_id", - column2="settlement_id", - copy=False, + settlement_line_partial_ids = fields.One2many( + "sale.commission.settlement.line.partial", + "invoice_agent_partial_id", + compute="_compute_settlement_line_partial_ids", + store=True, ) - account_partial_reconcile_id = fields.Many2one("account.partial.reconcile") + account_partial_reconcile_id = fields.Many2one( + "account.partial.reconcile" + ) # TODO: Remove amount = fields.Monetary( + compute="_compute_amount", + store=True, string="Commission Amount", ) currency_id = fields.Many2one( related="invoice_line_agent_id.currency_id", ) + settled_amount = fields.Monetary( + compute="_compute_settled_amount", + store=True, + ) + is_settled = fields.Boolean( + compute="_compute_settled_amount", store=True, string="Fully settled" + ) + + move_id = fields.Many2one(related="move_line_id.move_id", string="Invoice") + date_maturity = fields.Date( + related="move_line_id.date_maturity", + store=True, + ) + invoice_line_id = fields.Many2one( + related="invoice_line_agent_id.object_id", string="Invoice Line" + ) + agent_id = fields.Many2one( + related="invoice_line_agent_id.agent_id", + store=True, + ) + invoice_date = fields.Date( + related="invoice_line_agent_id.invoice_date", + store=True, + ) + + @api.depends( + "settlement_line_partial_ids.amount", + "settlement_line_partial_ids.is_settled", + ) + def _compute_settled_amount(self): + for rec in self: + # TODO: handle different currencies + rec.settled_amount = sum( + x.amount for x in rec.settlement_line_partial_ids if x.is_settled + ) + rec.is_settled = rec.currency_id.is_zero(rec.settled_amount - rec.amount) + + @api.depends( + "move_line_id.balance", + "move_line_id.move_id.amount_total", + "invoice_line_agent_id.amount", + ) + def _compute_amount(self): + for rec in self: + rec.amount = ( + rec.move_line_id.balance + * rec.invoice_line_agent_id.amount + / rec.move_line_id.move_id.amount_total + ) + + @api.depends( + "move_line_id.matched_debit_ids", + "move_line_id.matched_credit_ids", + ) + def _compute_settlement_line_partial_ids(self): + """ + Cf. method _get_reconciled_invoices_partials + in odoo.addons.account.models.account_move.AccountMove. + """ + for rec in self: + pay_term_line = rec.move_line_id + matched_partials = ( + pay_term_line.matched_debit_ids + pay_term_line.matched_credit_ids + ) + if not matched_partials: + continue + existing_partial_settlements = rec.settlement_line_partial_ids + existing_partials = existing_partial_settlements.mapped( + "partial_reconcile_id" + ) + + for partial in matched_partials: + if partial not in existing_partials: + existing_partial_settlements.create( + { + "partial_reconcile_id": partial.id, + "invoice_agent_partial_id": rec.id, + } + ) diff --git a/sale_commission_partial_settlement/models/account_partial_reconcile.py b/sale_commission_partial_settlement/models/account_partial_reconcile.py index e603c6baf..58c51eeeb 100644 --- a/sale_commission_partial_settlement/models/account_partial_reconcile.py +++ b/sale_commission_partial_settlement/models/account_partial_reconcile.py @@ -1,26 +1,26 @@ -from odoo import api, fields, models +from odoo import fields, models class AccountPartialReconcile(models.Model): _inherit = "account.partial.reconcile" - # Logically a One2one account_invoice_line_agent_partial_ids = fields.One2many( "account.invoice.line.agent.partial", "account_partial_reconcile_id" - ) - partial_commission_settled = fields.Boolean( - compute="_compute_partial_commission_settled", store=True - ) + ) # TODO: Remove? + # partial_commission_settled = fields.Boolean( + # compute="_compute_partial_commission_settled", store=True + # ) - @api.depends( - "account_invoice_line_agent_partial_ids", - "account_invoice_line_agent_partial_ids.agent_line.settlement_id.state", - ) - def _compute_partial_commission_settled(self): - for rec in self: - rec.partial_commission_settled = any( - settlement.state != "cancel" - for settlement in rec.mapped( - "account_invoice_line_agent_partial_ids.agent_line.settlement_id" - ) - ) + # APR can't tell if every agent was settled! + # @api.depends( + # "account_invoice_line_agent_partial_ids", + # "account_invoice_line_agent_partial_ids.agent_line.settlement_id.state", + # ) + # def _compute_partial_commission_settled(self): + # for rec in self: + # rec.partial_commission_settled = any( + # settlement.state != "cancel" + # for settlement in rec.mapped( + # "account_invoice_line_agent_partial_ids.agent_line.settlement_id" + # ) + # ) diff --git a/sale_commission_partial_settlement/models/sale_commission_settlement.py b/sale_commission_partial_settlement/models/sale_commission_settlement.py index 0f5225704..05e748047 100644 --- a/sale_commission_partial_settlement/models/sale_commission_settlement.py +++ b/sale_commission_partial_settlement/models/sale_commission_settlement.py @@ -18,6 +18,6 @@ class SaleCommissionSettlement(models.Model): help="The payment date used to create the settlement", ) - def unlink(self): - self.mapped("line_ids.agent_line_partial_ids").unlink() - return super().unlink() + # def unlink(self): + # self.mapped("line_ids.agent_line_partial_ids").unlink() + # return super().unlink() diff --git a/sale_commission_partial_settlement/models/sale_commission_settlement_line.py b/sale_commission_partial_settlement/models/sale_commission_settlement_line.py index a5ebda029..f1ca3c081 100644 --- a/sale_commission_partial_settlement/models/sale_commission_settlement_line.py +++ b/sale_commission_partial_settlement/models/sale_commission_settlement_line.py @@ -4,12 +4,16 @@ class SettlementLine(models.Model): _inherit = "sale.commission.settlement.line" - agent_line_partial_ids = fields.Many2many( + agent_line_partial_ids = fields.Many2many( # TODO: Remove? comodel_name="account.invoice.line.agent.partial", relation="settlement_agent_line_partial_rel", column1="settlement_id", column2="agent_line_partial_id", ) + settlement_line_partial_ids = fields.Many2many( + comodel_name="sale.commission.settlement.line.partial", + relation="settlement_line_line_partial_rel", + ) settled_amount = fields.Monetary( compute="_compute_settled_amount", related=False, @@ -21,10 +25,19 @@ class SettlementLine(models.Model): def _compute_settled_amount(self): for rec in self: if rec.commission_id.payment_amount_type == "paid": - rec.settled_amount = rec.agent_line_partial_ids[:1].amount + if rec.settlement_line_partial_ids: + rec.settled_amount = sum( + x.amount for x in rec.settlement_line_partial_ids + ) + else: # TODO: Remove? + rec.settled_amount = rec.agent_line_partial_ids[:1].amount else: rec.settled_amount = rec.agent_line[:1].amount - def unlink(self): - self.mapped("agent_line_partial_ids").unlink() - return super().unlink() + # def unlink(self): + # """ + # deprecated + # TODO: migrate? + # """ + # self.mapped("agent_line_partial_ids").unlink() + # return super().unlink() diff --git a/sale_commission_partial_settlement/models/sale_commission_settlement_line_partial.py b/sale_commission_partial_settlement/models/sale_commission_settlement_line_partial.py new file mode 100644 index 000000000..b4fa94b34 --- /dev/null +++ b/sale_commission_partial_settlement/models/sale_commission_settlement_line_partial.py @@ -0,0 +1,65 @@ +from odoo import api, fields, models + + +class SettlementLinePartial(models.Model): + _name = "sale.commission.settlement.line.partial" + _description = "Partial settlements. " + "Tracks the effective settled amounts relative to the expected." + + settlement_line_ids = fields.Many2many( + comodel_name="sale.commission.settlement.line", + relation="settlement_line_line_partial_rel", + ) + invoice_agent_partial_id = fields.Many2one( + comodel_name="account.invoice.line.agent.partial", + required=True, + ) + partial_reconcile_id = fields.Many2one( + comodel_name="account.partial.reconcile", required=True, ondelete="cascade" + ) + amount = fields.Monetary( + compute="_compute_amount", + store=True, + ) + move_id = fields.Many2one(related="invoice_agent_partial_id.move_id") + company_id = fields.Many2one(related="move_id.company_id") + invoice_line_id = fields.Many2one( + related="invoice_agent_partial_id.invoice_line_id" + ) + invoice_date = fields.Date( + related="invoice_agent_partial_id.invoice_date", store=True + ) + invoice_line_agent_id = fields.Many2one( + related="invoice_agent_partial_id.invoice_line_agent_id" + ) + agent_id = fields.Many2one( + related="invoice_agent_partial_id.agent_id", store=True, index=True + ) + currency_id = fields.Many2one(related="invoice_agent_partial_id.currency_id") + reconcile_amount = fields.Monetary( + related="partial_reconcile_id.amount", string="Payment amount" + ) + date_maturity = fields.Date( + related="partial_reconcile_id.max_date", store=True, index=True + ) + is_settled = fields.Boolean(compute="_compute_is_settled", store=True, index=True) + + @api.depends( + "partial_reconcile_id.amount", + "invoice_agent_partial_id.invoice_line_agent_id.amount", + "invoice_agent_partial_id.move_id.amount_total", + ) + def _compute_amount(self): + for rec in self: + rec.amount = ( + rec.partial_reconcile_id.amount + * rec.invoice_agent_partial_id.invoice_line_agent_id.amount + / rec.invoice_agent_partial_id.move_id.amount_total + ) + + @api.depends("settlement_line_ids.settlement_id.state") + def _compute_is_settled(self): + for rec in self: + rec.is_settled = any( + x and x.settlement_id.state != "cancel" for x in rec.settlement_line_ids + ) diff --git a/sale_commission_partial_settlement/security/ir.model.access.csv b/sale_commission_partial_settlement/security/ir.model.access.csv index 4c54861bb..81a6173bf 100644 --- a/sale_commission_partial_settlement/security/ir.model.access.csv +++ b/sale_commission_partial_settlement/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_account_invoice_line_agent_partial,access_account_invoice_line_agent_partial,model_account_invoice_line_agent_partial,sales_team.group_sale_salesman,1,1,1,1 access_account_invoice_line_agent_partial_user,access_account_invoice_line_agent_partial_user,model_account_invoice_line_agent_partial,base.group_user,1,0,0,0 +access_sale_commission_settlement_line_partial,access_sale_commission_settlement_line_partial,model_sale_commission_settlement_line_partial,sales_team.group_sale_salesman,1,1,1,1 +access_sale_commission_settlement_line_partial_user,access_sale_commission_settlement_line_partial_user,model_sale_commission_settlement_line_partial,base.group_user,1,0,0,0 diff --git a/sale_commission_partial_settlement/tests/test_partial_settlement.py b/sale_commission_partial_settlement/tests/test_partial_settlement.py index 66ba38ea4..8e1201b39 100644 --- a/sale_commission_partial_settlement/tests/test_partial_settlement.py +++ b/sale_commission_partial_settlement/tests/test_partial_settlement.py @@ -142,11 +142,13 @@ def _invoice_sale_order(self, sale_order, date=None): def _settle_agent(self, agent=None, period=None, date=None, date_payment_to=None): vals = { "date_to": ( - fields.Datetime.from_string(fields.Datetime.now()) - + relativedelta(months=period) - ) - if period - else date, + ( + fields.Datetime.from_string(fields.Datetime.now()) + + relativedelta(months=period) + ) + if period + else date + ), "date_payment_to": date_payment_to, } if agent: diff --git a/sale_commission_partial_settlement/views/account_invoice_line_agent_partial_views.xml b/sale_commission_partial_settlement/views/account_invoice_line_agent_partial_views.xml new file mode 100644 index 000000000..ee250317f --- /dev/null +++ b/sale_commission_partial_settlement/views/account_invoice_line_agent_partial_views.xml @@ -0,0 +1,42 @@ + + + + invoice.line.agent.partial.tree.embedded + account.invoice.line.agent.partial + 999 + + + + + + + + + + + + + invoice.line.agent.partial.tree + account.invoice.line.agent.partial + + primary + + + + + + + + + Partial Commission Forecast + account.invoice.line.agent.partial + tree + + + diff --git a/sale_commission_partial_settlement/views/account_invoice_line_agent_views.xml b/sale_commission_partial_settlement/views/account_invoice_line_agent_views.xml new file mode 100644 index 000000000..62d1e2c0c --- /dev/null +++ b/sale_commission_partial_settlement/views/account_invoice_line_agent_views.xml @@ -0,0 +1,52 @@ + + + + account.invoice.line.agent + + + + +