Skip to content

Commit

Permalink
[FIX] sale_commission_partial_settlement: major refactor
Browse files Browse the repository at this point in the history
This commit refactors the data structure of the module entirely,
in order to provide both a forecast and more detailed
traceability of partial commissions, from invoice to
settlement.

The new data structure is a better fit for the way invoices
and moves work in base Odoo as well.
  • Loading branch information
aleuffre committed Jan 22, 2025
1 parent 4ed5bc0 commit 3996099
Show file tree
Hide file tree
Showing 19 changed files with 624 additions and 178 deletions.
14 changes: 13 additions & 1 deletion sale_commission_partial_settlement/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Sales commissions based on paid amount
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:9596c13efc487356fc34bac45ca9c0138c0ec5127e6e12f5427aea0a1630db80
!! source digest: sha256:a9e800ab8099964a8184f688597f54dba7b6f40f0110cac40e8d1e60576186e0
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down Expand Up @@ -44,6 +44,18 @@ Usage

Behavior for commission settlements will be as per module description.

Known issues / Roadmap
======================

Due to the data structure of the module before version 14.0.2.0.0,
there is no way to link partial settlements created before this version
of the module to a specific payment or account move, and therefore to the
new data structure existing after the changes.

A "best effort" migration of previous data could be developed to
ensure a smoother transition between versions.


Bug Tracker
===========

Expand Down
5 changes: 4 additions & 1 deletion sale_commission_partial_settlement/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

from openupgradelib import openupgrade

_logger = logging.getLogger(__name__)


def recompute_partial_commission_forecast(env):
"""
Recompute forecast for all partial commissions
"""
_logger.info(
"Computing partial commission forecast for all invoices. "
"This may take some time..."
)
limit = 5000
offset = 0
while True:
aila = env["account.invoice.line.agent"].search([], limit=limit, offset=offset)
if not aila:
break
offset += limit
aila._compute_invoice_line_agent_partial_ids()
while env.all.tocompute:
aila.flush()
aila.invalidate_cache() # avoid MemoryError
_logger.info(
"%(offset)s invoice commission lines computed!" % {"offset": str(offset)}
)
_logger.info("Computation of partial commission forecast completed!")


@openupgrade.migrate()
def migrate(env, version):
recompute_partial_commission_forecast(env)
1 change: 1 addition & 0 deletions sale_commission_partial_settlement/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,40 +1,45 @@
# 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",
store=True,
)
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(
"commission_id.payment_amount_type", "amount", "settled", "partial_settled"
"commission_id.payment_amount_type",
"amount",
"settled",
"partial_settled",
)
def _compute_is_fully_settled(self):
for rec in self:
Expand All @@ -50,62 +55,51 @@ def _compute_is_fully_settled(self):
== 0
)

def _partial_commissions(self, date_payment_to):
@api.depends(
"amount",
"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:
# Prevent compute from running too early
if not rec.id:
continue
ailap_model = rec.invoice_line_agent_partial_ids.browse()
if rec.commission_id.payment_amount_type != "paid" or rec.amount == 0:
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,
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,124 @@
# 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:
# move_line_id.balance
# invoice_line_agent_id.amount
# move_line_id.move_id.amount_total_signed
# all 3 terms are signed
rec.amount = (
rec.move_line_id.balance
* rec.invoice_line_agent_id.amount
/ rec.move_line_id.move_id.amount_total_signed
)

@api.depends(
"invoice_line_agent_id.amount",
"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:
if not rec.invoice_line_agent_id.amount:
rec.settlement_line_partial_ids = False
continue
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,
"agent_line": [(4, rec.invoice_line_agent_id.id)],
}
)
Loading

0 comments on commit 3996099

Please sign in to comment.