diff --git a/.copier-answers.yml b/.copier-answers.yml index 85280cc5f..e8f8a6e99 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: v1.11.0 +_commit: v1.11.1 _src_path: https://github.com/OCA/oca-addons-repo-template.git ci: GitHub dependency_installation_mode: PIP diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c74928c7..f8b0a807b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -104,7 +104,7 @@ repos: - --settings=. exclude: /__init__\.py$ - repo: https://github.com/acsone/setuptools-odoo - rev: 3.0.3 + rev: 3.1.8 hooks: - id: setuptools-odoo-make-default - id: setuptools-odoo-get-requirements @@ -113,7 +113,7 @@ repos: - requirements.txt - --header - "# generated from manifests external_dependencies" - - repo: https://gitlab.com/PyCQA/flake8 + - repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 diff --git a/account_commission/README.rst b/account_commission/README.rst new file mode 100644 index 000000000..56765f8ee --- /dev/null +++ b/account_commission/README.rst @@ -0,0 +1,132 @@ +=================== +Account commissions +=================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcommission-lightgray.png?logo=github + :target: https://github.com/OCA/commission/tree/15.0/account_commission + :alt: OCA/commission +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/commission-15-0/commission-15-0-account_commission + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/165/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds the function to calculate commissions in invoices (account moves). + +This module depends on the commission module. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +For selecting invoice status in commissions: + +#. Edit or create a new record to select the invoice status for settling the commissions. + + * **Invoice Based**: Commissions are settled when the invoice is issued. + * **Payment Based**: Commissions are settled when the invoice is paid. + +Usage +===== + +For adding commissions on invoices: + +#. Go to *Invoicing > Customers > Invoices*. +#. Edit or create a new record. +#. When you have selected a partner, each new invoice line you add will have + the agents and commissions set at customer level. +#. You can add, modify or delete these agents discretely clicking on the + icon with several persons represented, next to the "Commission" field in the + list. This icon will be available only if the line hasn't been invoiced yet. +#. If you have configured your system for editing lines in a popup window, + agents will appear also in this window. +#. The agents icon will be in this ocassion visible when the line hasn't been + settled. +#. You have a button "Regenerate agents" on the bottom of the page + "Invoice Lines" for forcing a recompute of all agents from the partner setup. + This is needed for example when you have changed the partner on the + invoice having already inserted lines. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* Pexego. +* Davide Corio +* Joao Alfredo Gama Batista +* Sandy Carter +* Giorgio Borelli +* Daniel Campos +* Oihane Crucelaegui +* Nicola Malcontenti +* Aitor Bouzas + +* `Tecnativa `__: + + * Pedro M. Baeza + * Manuel Calero + +* `Quartile `__: + + * Aung Ko Ko Lin + * Yoshi Tashiro + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px + :target: https://github.com/pedrobaeza + :alt: pedrobaeza + +Current `maintainer `__: + +|maintainer-pedrobaeza| + +This module is part of the `OCA/commission `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_commission/__init__.py b/account_commission/__init__.py new file mode 100644 index 000000000..7c4c8a90a --- /dev/null +++ b/account_commission/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import wizards +from . import report diff --git a/account_commission/__manifest__.py b/account_commission/__manifest__.py new file mode 100644 index 000000000..f461820f9 --- /dev/null +++ b/account_commission/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2014-2020 Tecnativa - Pedro M. Baeza +# Copyright 2020 Tecnativa - Manuel Calero +{ + "name": "Account commissions", + "version": "15.0.1.0.0", + "author": "Tecnativa," "Odoo Community Association (OCA)", + "category": "Sales Management", + "license": "AGPL-3", + "depends": [ + "account", + "commission", + ], + "website": "https://github.com/OCA/commission", + "maintainers": ["pedrobaeza"], + "data": [ + "security/ir.model.access.csv", + "security/account_commission_security.xml", + "views/account_move_views.xml", + "views/account_commission_settlement_view.xml", + "views/commission_views.xml", + "views/report_settlement_templates.xml", + "views/res_partner.xml", + "report/commission_analysis_view.xml", + ], + "installable": True, +} diff --git a/account_commission/models/__init__.py b/account_commission/models/__init__.py new file mode 100644 index 000000000..bffca9a74 --- /dev/null +++ b/account_commission/models/__init__.py @@ -0,0 +1,4 @@ +from . import account_move +from . import commission +from . import settlement +from . import res_partner diff --git a/account_commission/models/account_move.py b/account_commission/models/account_move.py new file mode 100644 index 000000000..6735d9b25 --- /dev/null +++ b/account_commission/models/account_move.py @@ -0,0 +1,209 @@ +# Copyright 2014-2018 Tecnativa - Pedro M. Baeza +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from lxml import etree + +from odoo import _, api, exceptions, fields, models + + +class AccountMove(models.Model): + _inherit = "account.move" + + commission_total = fields.Float( + string="Commissions", + compute="_compute_commission_total", + store=True, + ) + partner_agent_ids = fields.Many2many( + string="Agents", + comodel_name="res.partner", + compute="_compute_agents", + search="_search_agents", + ) + + @api.depends("partner_agent_ids", "invoice_line_ids.agent_ids.agent_id") + def _compute_agents(self): + for move in self: + move.partner_agent_ids = [ + (6, 0, move.mapped("invoice_line_ids.agent_ids.agent_id").ids) + ] + + @api.model + def _search_agents(self, operator, value): + ail_agents = self.env["account.invoice.line.agent"].search( + [("agent_id", operator, value)] + ) + return [("id", "in", ail_agents.mapped("object_id.move_id").ids)] + + @api.depends("line_ids.agent_ids.amount") + def _compute_commission_total(self): + for record in self: + record.commission_total = 0.0 + for line in record.line_ids: + record.commission_total += sum(x.amount for x in line.agent_ids) + + def button_cancel(self): + """Check settled lines.""" + if any(self.mapped("invoice_line_ids.any_settled")): + raise exceptions.ValidationError( + _("You can't cancel an invoice with settled lines"), + ) + return super().button_cancel() + + def recompute_lines_agents(self): + self.mapped("invoice_line_ids").recompute_agents() + + @api.model + def fields_view_get( + self, view_id=None, view_type="form", toolbar=False, submenu=False + ): + """Inject in this method the needed context for not removing other + possible context values. + """ + res = super(AccountMove, self).fields_view_get( + view_id=view_id, + view_type=view_type, + toolbar=toolbar, + submenu=submenu, + ) + if view_type == "form": + invoice_xml = etree.XML(res["arch"]) + invoice_line_fields = invoice_xml.xpath("//field[@name='invoice_line_ids']") + if invoice_line_fields: + invoice_line_field = invoice_line_fields[0] + context = invoice_line_field.attrib.get("context", "{}").replace( + "{", + "{'partner_id': partner_id, ", + 1, + ) + invoice_line_field.attrib["context"] = context + res["arch"] = etree.tostring(invoice_xml) + return res + + +class AccountMoveLine(models.Model): + _inherit = [ + "account.move.line", + "commission.mixin", + ] + _name = "account.move.line" + + agent_ids = fields.One2many(comodel_name="account.invoice.line.agent") + any_settled = fields.Boolean(compute="_compute_any_settled") + + @api.depends("agent_ids", "agent_ids.settled") + def _compute_any_settled(self): + for record in self: + record.any_settled = any(record.mapped("agent_ids.settled")) + + @api.depends("move_id.partner_id") + def _compute_agent_ids(self): + for res in self: + settlement_lines = self.env["commission.settlement.line"].search( + [("invoice_line_id", "=", res.id)] + ) + for line in settlement_lines: + line.date = False + line.agent_id = False + line.settled_amount = 0.00 + line.currency_id = False + line.commission_id = False + self.agent_ids = False # for resetting previous agents + for record in self.filtered( + lambda x: x.move_id.partner_id and x.move_id.move_type[:3] == "out" + ): + if not record.commission_free and record.product_id: + record.agent_ids = record._prepare_agents_vals_partner( + record.move_id.partner_id + ) + + +class AccountInvoiceLineAgent(models.Model): + _inherit = "commission.line.mixin" + _name = "account.invoice.line.agent" + _description = "Agent detail of commission line in invoice lines" + + object_id = fields.Many2one(comodel_name="account.move.line") + invoice_id = fields.Many2one( + string="Invoice", + comodel_name="account.move", + related="object_id.move_id", + store=True, + ) + invoice_date = fields.Date( + string="Invoice date", + related="invoice_id.invoice_date", + store=True, + readonly=True, + ) + agent_line = fields.Many2many( + comodel_name="commission.settlement.line", + relation="settlement_agent_line_rel", + column1="agent_line_id", + column2="settlement_id", + copy=False, + ) + settled = fields.Boolean(compute="_compute_settled", store=True) + company_id = fields.Many2one( + comodel_name="res.company", + compute="_compute_company", + store=True, + ) + currency_id = fields.Many2one( + related="object_id.currency_id", + readonly=True, + ) + + @api.depends( + "object_id.price_subtotal", + "object_id.product_id.commission_free", + "commission_id", + ) + def _compute_amount(self): + for line in self: + inv_line = line.object_id + line.amount = line._get_commission_amount( + line.commission_id, + inv_line.price_subtotal, + inv_line.product_id, + inv_line.quantity, + ) + # Refunds commissions are negative + if line.invoice_id.move_type and "refund" in line.invoice_id.move_type: + line.amount = -line.amount + + @api.depends( + "agent_line", "agent_line.settlement_id.state", "invoice_id", "invoice_id.state" + ) + def _compute_settled(self): + # Count lines of not open or paid invoices as settled for not + # being included in settlements + for line in self: + line.settled = any( + x.settlement_id.state != "cancel" for x in line.agent_line + ) + + @api.depends("object_id", "object_id.company_id") + def _compute_company(self): + for line in self: + line.company_id = line.object_id.company_id + + @api.constrains("agent_id", "amount") + def _check_settle_integrity(self): + for record in self: + if any(record.mapped("settled")): + raise exceptions.ValidationError( + _("You can't modify a settled line"), + ) + + def _skip_settlement(self): + """This function should return False if the commission can be paid. + + :return: bool + """ + self.ensure_one() + return ( + self.commission_id.invoice_state == "paid" + and self.invoice_id.payment_state not in ["in_payment", "paid"] + ) or self.invoice_id.state != "posted" diff --git a/account_commission/models/commission.py b/account_commission/models/commission.py new file mode 100644 index 000000000..056117b32 --- /dev/null +++ b/account_commission/models/commission.py @@ -0,0 +1,14 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class Commission(models.Model): + _inherit = "commission" + + invoice_state = fields.Selection( + [("open", "Invoice Based"), ("paid", "Payment Based")], + string="Invoice Status", + required=True, + default="open", + ) diff --git a/account_commission/models/res_partner.py b/account_commission/models/res_partner.py new file mode 100644 index 000000000..877cb56d5 --- /dev/null +++ b/account_commission/models/res_partner.py @@ -0,0 +1,17 @@ +# Copyright 2016-2019 Tecnativa - Pedro M. Baeza +# Copyright 2018 Tecnativa - Ernesto Tejeda +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html + +from odoo import fields, models + + +class ResPartner(models.Model): + """Add some fields related to commissions""" + + _inherit = "res.partner" + + settlement_ids = fields.One2many( + comodel_name="commission.settlement", + inverse_name="agent_id", + readonly=True, + ) diff --git a/account_commission/models/settlement.py b/account_commission/models/settlement.py new file mode 100644 index 000000000..3b2905619 --- /dev/null +++ b/account_commission/models/settlement.py @@ -0,0 +1,31 @@ +# Copyright 2014-2020 Tecnativa - Pedro M. Baeza +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SettlementLine(models.Model): + _inherit = "commission.settlement.line" + + agent_line = fields.Many2many( + comodel_name="account.invoice.line.agent", + relation="settlement_agent_line_rel", + column1="settlement_id", + column2="agent_line_id", + required=True, + ) + invoice_line_id = fields.Many2one( + comodel_name="account.move.line", + store=True, + related="agent_line.object_id", + string="Source invoice line", + ) + + @api.constrains("settlement_id", "agent_line") + def _check_company(self): + for record in self: + for line in record.agent_line: + if line.company_id != record.company_id: + raise UserError(_("Company must be the same")) diff --git a/account_commission/readme/CONFIGURE.rst b/account_commission/readme/CONFIGURE.rst new file mode 100644 index 000000000..78d97059d --- /dev/null +++ b/account_commission/readme/CONFIGURE.rst @@ -0,0 +1,6 @@ +For selecting invoice status in commissions: + +#. Edit or create a new record to select the invoice status for settling the commissions. + + * **Invoice Based**: Commissions are settled when the invoice is issued. + * **Payment Based**: Commissions are settled when the invoice is paid. diff --git a/account_commission/readme/CONTRIBUTORS.rst b/account_commission/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..77b751822 --- /dev/null +++ b/account_commission/readme/CONTRIBUTORS.rst @@ -0,0 +1,19 @@ +* Pexego. +* Davide Corio +* Joao Alfredo Gama Batista +* Sandy Carter +* Giorgio Borelli +* Daniel Campos +* Oihane Crucelaegui +* Nicola Malcontenti +* Aitor Bouzas + +* `Tecnativa `__: + + * Pedro M. Baeza + * Manuel Calero + +* `Quartile `__: + + * Aung Ko Ko Lin + * Yoshi Tashiro diff --git a/account_commission/readme/DESCRIPTION.rst b/account_commission/readme/DESCRIPTION.rst new file mode 100644 index 000000000..92ffbc61c --- /dev/null +++ b/account_commission/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module adds the function to calculate commissions in invoices (account moves). + +This module depends on the commission module. diff --git a/account_commission/readme/USAGE.rst b/account_commission/readme/USAGE.rst new file mode 100644 index 000000000..6ae855144 --- /dev/null +++ b/account_commission/readme/USAGE.rst @@ -0,0 +1,17 @@ +For adding commissions on invoices: + +#. Go to *Invoicing > Customers > Invoices*. +#. Edit or create a new record. +#. When you have selected a partner, each new invoice line you add will have + the agents and commissions set at customer level. +#. You can add, modify or delete these agents discretely clicking on the + icon with several persons represented, next to the "Commission" field in the + list. This icon will be available only if the line hasn't been invoiced yet. +#. If you have configured your system for editing lines in a popup window, + agents will appear also in this window. +#. The agents icon will be in this ocassion visible when the line hasn't been + settled. +#. You have a button "Regenerate agents" on the bottom of the page + "Invoice Lines" for forcing a recompute of all agents from the partner setup. + This is needed for example when you have changed the partner on the + invoice having already inserted lines. diff --git a/account_commission/report/__init__.py b/account_commission/report/__init__.py new file mode 100644 index 000000000..f93e938fb --- /dev/null +++ b/account_commission/report/__init__.py @@ -0,0 +1 @@ +from . import commission_analysis diff --git a/account_commission/report/commission_analysis.py b/account_commission/report/commission_analysis.py new file mode 100644 index 000000000..12e833503 --- /dev/null +++ b/account_commission/report/commission_analysis.py @@ -0,0 +1,106 @@ +# Copyright 2014-2018 Tecnativa - Pedro M. Baeza +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from psycopg2.extensions import AsIs + +from odoo import api, fields, models, tools + + +class InvoiceCommissionAnalysisReport(models.Model): + _name = "invoice.commission.analysis.report" + _description = "Invoice Commission Analysis Report" + _auto = False + _rec_name = "commission_id" + + @api.model + def _get_selection_invoice_state(self): + return self.env["account.move"].fields_get(allfields=["state"])["state"][ + "selection" + ] + + invoice_state = fields.Selection( + selection="_get_selection_invoice_state", string="Invoice Status", readonly=True + ) + date_invoice = fields.Date("Invoice Date", readonly=True) + company_id = fields.Many2one("res.company", "Company", readonly=True) + partner_id = fields.Many2one("res.partner", "Partner", readonly=True) + agent_id = fields.Many2one("res.partner", "Agent", readonly=True) + categ_id = fields.Many2one("product.category", "Category of Product", readonly=True) + product_id = fields.Many2one("product.product", "Product", readonly=True) + uom_id = fields.Many2one("uom.uom", "Unit of Measure", readonly=True) + quantity = fields.Float("# of Qty", readonly=True) + price_unit = fields.Float("Unit Price", readonly=True) + price_subtotal = fields.Float("Subtotal", readonly=True) + balance = fields.Float( + readonly=True, + ) + percentage = fields.Integer("Percentage of commission", readonly=True) + amount = fields.Float(readonly=True) + invoice_line_id = fields.Many2one("account.move.line", readonly=True) + settled = fields.Boolean(readonly=True) + commission_id = fields.Many2one("commission", "Commission", readonly=True) + + def _select(self): + select_str = """ + SELECT MIN(aila.id) AS id, + ai.partner_id AS partner_id, + ai.state AS invoice_state, + ai.date AS date_invoice, + ail.company_id AS company_id, + rp.id AS agent_id, + pt.categ_id AS categ_id, + ail.product_id AS product_id, + pt.uom_id AS uom_id, + SUM(ail.quantity) AS quantity, + AVG(ail.price_unit) AS price_unit, + SUM(ail.price_subtotal) AS price_subtotal, + SUM(ail.balance) AS balance, + AVG(c.fix_qty) AS percentage, + SUM(aila.amount) AS amount, + ail.id AS invoice_line_id, + aila.settled AS settled, + aila.commission_id AS commission_id + """ + return select_str + + def _from(self): + from_str = """ + account_invoice_line_agent aila + LEFT JOIN account_move_line ail ON ail.id = aila.object_id + INNER JOIN account_move ai ON ai.id = ail.move_id + LEFT JOIN commission c ON c.id = aila.commission_id + LEFT JOIN product_product pp ON pp.id = ail.product_id + INNER JOIN product_template pt ON pp.product_tmpl_id = pt.id + LEFT JOIN res_partner rp ON aila.agent_id = rp.id + """ + return from_str + + def _group_by(self): + group_by_str = """ + GROUP BY ai.partner_id, + ai.state, + ai.date, + ail.company_id, + rp.id, + pt.categ_id, + ail.product_id, + pt.uom_id, + ail.id, + aila.settled, + aila.commission_id + """ + return group_by_str + + @api.model + def init(self): + tools.drop_view_if_exists(self._cr, self._table) + self._cr.execute( + "CREATE or REPLACE VIEW %s AS ( %s FROM ( %s ) %s )", + ( + AsIs(self._table), + AsIs(self._select()), + AsIs(self._from()), + AsIs(self._group_by()), + ), + ) diff --git a/account_commission/report/commission_analysis_view.xml b/account_commission/report/commission_analysis_view.xml new file mode 100644 index 000000000..8a72ee2ad --- /dev/null +++ b/account_commission/report/commission_analysis_view.xml @@ -0,0 +1,122 @@ + + + + invoice.commission.analysis.pivot + invoice.commission.analysis.report + + + + + + + + + + invoice.commission.analysis.graph + invoice.commission.analysis.report + + + + + + + + + invoice.commission.analysis.search + invoice.commission.analysis.report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Commission Analysis + invoice.commission.analysis.report + pivot,graph + + + This report performs analysis on your commissions added in invoice lines. You can check the amount and lines that will be settled by date, product, product category, aget, etc. Use this report to perform analysis on invoice lines agents not beeing settled yet. + + + diff --git a/account_commission/security/account_commission_security.xml b/account_commission/security/account_commission_security.xml new file mode 100644 index 000000000..f3fe99133 --- /dev/null +++ b/account_commission/security/account_commission_security.xml @@ -0,0 +1,13 @@ + + + + + Invoice commission line multi-company + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + diff --git a/account_commission/security/ir.model.access.csv b/account_commission/security/ir.model.access.csv new file mode 100644 index 000000000..3ab48f625 --- /dev/null +++ b/account_commission/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_invoice_line_agent,access_account_invoice_line_agent,model_account_invoice_line_agent,account.group_account_invoice,1,1,1,1 +access_account_invoice_line_agent_user,access_account_invoice_line_agent_user,model_account_invoice_line_agent,base.group_user,1,0,0,0 +access_invoice_commission_analysis_report,access_invoice_commission_analysis_report,model_invoice_commission_analysis_report,base.group_user,1,0,0,0 diff --git a/account_commission/static/description/icon.png b/account_commission/static/description/icon.png new file mode 100644 index 000000000..053992789 Binary files /dev/null and b/account_commission/static/description/icon.png differ diff --git a/account_commission/static/description/icon.svg b/account_commission/static/description/icon.svg new file mode 100644 index 000000000..3e224be4e --- /dev/null +++ b/account_commission/static/description/icon.svg @@ -0,0 +1,2804 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + Pile of Golden Coins + 2010-04-09T03:27:45 + A pile of hypothetical golden coins, drawn in Inkscape. + https://openclipart.org/detail/43969/pile-of-golden-coins-by-j_alves + + + J_Alves + + + + + coin + currency + gold + money + thaler + + + + + + + + + + + diff --git a/account_commission/static/description/index.html b/account_commission/static/description/index.html new file mode 100644 index 000000000..b920424dc --- /dev/null +++ b/account_commission/static/description/index.html @@ -0,0 +1,474 @@ + + + + + + +Account commissions + + + +
+

Account commissions

+ + +

Beta License: AGPL-3 OCA/commission Translate me on Weblate Try me on Runbot

+

This module adds the function to calculate commissions in invoices (account moves).

+

This module depends on the commission module.

+

Table of contents

+ +
+

Configuration

+

For selecting invoice status in commissions:

+
    +
  1. Edit or create a new record to select the invoice status for settling the commissions.
      +
    • Invoice Based: Commissions are settled when the invoice is issued.
    • +
    • Payment Based: Commissions are settled when the invoice is paid.
    • +
    +
  2. +
+
+
+

Usage

+

For adding commissions on invoices:

+
    +
  1. Go to Invoicing > Customers > Invoices.
  2. +
  3. Edit or create a new record.
  4. +
  5. When you have selected a partner, each new invoice line you add will have +the agents and commissions set at customer level.
  6. +
  7. You can add, modify or delete these agents discretely clicking on the +icon with several persons represented, next to the “Commission” field in the +list. This icon will be available only if the line hasn’t been invoiced yet.
  8. +
  9. If you have configured your system for editing lines in a popup window, +agents will appear also in this window.
  10. +
  11. The agents icon will be in this ocassion visible when the line hasn’t been +settled.
  12. +
  13. You have a button “Regenerate agents” on the bottom of the page +“Invoice Lines” for forcing a recompute of all agents from the partner setup. +This is needed for example when you have changed the partner on the +invoice having already inserted lines.
  14. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

pedrobaeza

+

This module is part of the OCA/commission project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_commission/tests/__init__.py b/account_commission/tests/__init__.py new file mode 100644 index 000000000..4833fb2de --- /dev/null +++ b/account_commission/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_commission diff --git a/account_commission/tests/test_account_commission.py b/account_commission/tests/test_account_commission.py new file mode 100644 index 000000000..04fe516ea --- /dev/null +++ b/account_commission/tests/test_account_commission.py @@ -0,0 +1,395 @@ +# Copyright 2016-2019 Tecnativa - Pedro M. Baeza +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests import tagged +from odoo.tests.common import Form + +from odoo.addons.commission.tests.test_commission import TestCommission + + +@tagged("post_install", "-at_install") +class TestAccountCommission(TestCommission): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.commission_net_paid.write({"invoice_state": "paid"}) + cls.commission_net_invoice = cls.commission_model.create( + { + "name": "10% fixed commission (Net amount) - Invoice Based", + "fix_qty": 10.0, + "amount_base_type": "net_amount", + } + ) + cls.commission_section_paid.write({"invoice_state": "paid"}) + cls.product = cls.env.ref("product.product_product_5") + cls.product.list_price = 5 # for testing specific commission section + cls.default_line_account = cls.env.ref("account.data_account_type_receivable") + cls.agent_biweekly = cls.res_partner_model.create( + { + "name": "Test Agent - Bi-weekly", + "agent": True, + "settlement": "biweekly", + "lang": "en_US", + "commission_id": cls.commission_net_invoice.id, + } + ) + cls.income_account = cls.env["account.account"].search( + [ + ("company_id", "=", cls.company.id), + ("user_type_id.name", "=", "Income"), + ], + limit=1, + ) + + def _create_invoice(self, agent, commission, date=None): + invoice = self.env["account.move"].create( + { + "partner_id": self.partner.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "product_id": self.product.id, + "quantity": 1.0, + "account_id": self.default_line_account.id, + "price_unit": self.product.lst_price, + "agent_ids": [ + ( + 0, + 0, + { + "agent_id": agent.id, + "commission_id": commission.id, + }, + ) + ], + }, + ) + ], + } + ) + if date: + invoice.invoice_date = date + invoice.date = date + return invoice + + def _settle_agent_invoice(self, agent=None, period=None, date=None): + vals = self._get_make_settle_vals(agent, period, date) + vals["settlement_type"] = "invoice" + wizard = self.make_settle_model.create(vals) + wizard.action_settle() + + def _process_invoice_and_settle(self, agent, commission, period, order=None): + if not order: + invoice = self._create_invoice(agent, commission) + else: + invoice = order.invoice_ids + invoice.invoice_line_ids.agent_ids._compute_amount() + invoice.action_post() + self._settle_agent_invoice(agent, period) + return invoice + + def _check_invoice_thru_settle( + self, agent, commission, period, initial_count, order=None + ): + invoice = self._process_invoice_and_settle(agent, commission, period, order) + settlements = self.settle_model.search([("state", "=", "settled")]) + self.assertEqual(len(settlements), initial_count) + journal = self.env["account.journal"].search( + [("type", "=", "cash"), ("company_id", "=", invoice.company_id.id)], + limit=1, + ) + register_payments = ( + self.env["account.payment.register"] + .with_context(active_ids=invoice.id, active_model="account.move") + .create({"journal_id": journal.id}) + ) + register_payments.action_create_payments() + self.assertEqual(invoice.partner_agent_ids.ids, agent.ids) + self.assertEqual( + self.env["account.move"] + .search([("partner_agent_ids", "=", agent.name)]) + .ids, + invoice.ids, + ) + self.assertIn(invoice.payment_state, ["in_payment", "paid"]) + self._settle_agent_invoice(agent, period) + settlements = self.settle_model.search([("state", "=", "settled")]) + self.assertTrue(settlements) + inv_line = invoice.invoice_line_ids[0] + self.assertTrue(inv_line.any_settled) + with self.assertRaises(ValidationError): + inv_line.agent_ids.amount = 5 + return self._check_settlements(agent, commission, settlements) + + def test_account_commission_gross_amount_payment(self): + self._check_invoice_thru_settle( + self.env.ref("commission.res_partner_pritesh_sale_agent"), + self.commission_section_paid, + 1, + 0, + ) + + def test_account_commission_gross_amount_payment_annual(self): + self._check_invoice_thru_settle( + self.agent_annual, self.commission_section_paid, 12, 0 + ) + + def test_account_commission_gross_amount_payment_semi(self): + self.product.list_price = 15100 # for testing specific commission section + self._check_invoice_thru_settle( + self.agent_semi, self.commission_section_invoice, 6, 1 + ) + + def test_account_commission_gross_amount_invoice(self): + self._process_invoice_and_settle( + self.agent_quaterly, + self.env.ref("commission.demo_commission"), + 1, + ) + settlements = self.settle_model.search([("state", "=", "invoiced")]) + settlements.make_invoices(self.journal, self.commission_product) + for settlement in settlements: + self.assertNotEqual( + len(settlement.invoice_id), + 0, + "Settlements need to be in Invoiced State.", + ) + + def test_commission_status(self): + # Make sure user is in English + self.env.user.lang = "en_US" + invoice = self._create_invoice( + self.env.ref("commission.res_partner_pritesh_sale_agent"), + self.commission_section_invoice, + ) + self.assertIn("1", invoice.invoice_line_ids[0].commission_status) + self.assertNotIn("agents", invoice.invoice_line_ids[0].commission_status) + invoice.mapped("invoice_line_ids.agent_ids").unlink() + self.assertIn("No", invoice.invoice_line_ids[0].commission_status) + invoice.invoice_line_ids[0].agent_ids = [ + ( + 0, + 0, + { + "agent_id": self.env.ref( + "commission.res_partner_pritesh_sale_agent" + ).id, + "commission_id": self.env.ref("commission.demo_commission").id, + }, + ), + ( + 0, + 0, + { + "agent_id": self.env.ref( + "commission.res_partner_eiffel_sale_agent" + ).id, + "commission_id": self.env.ref("commission.demo_commission").id, + }, + ), + ] + self.assertIn("2", invoice.invoice_line_ids[0].commission_status) + self.assertIn("agents", invoice.invoice_line_ids[0].commission_status) + invoice.action_post() + # Free + invoice.invoice_line_ids.commission_free = True + self.assertIn("free", invoice.invoice_line_ids.commission_status) + self.assertAlmostEqual(invoice.invoice_line_ids.agent_ids.amount, 0) + # test show agents buton + action = invoice.invoice_line_ids.button_edit_agents() + self.assertEqual(action["res_id"], invoice.invoice_line_ids.id) + + def test_supplier_invoice(self): + """No agents should be populated on supplier invoices.""" + self.partner.agent_ids = self.agent_semi + move_form = Form( + self.env["account.move"].with_context(default_move_type="in_invoice") + ) + move_form.partner_id = self.partner + move_form.ref = "sale_comission_TEST" + with move_form.invoice_line_ids.new() as line_form: + line_form.product_id = self.product + line_form.quantity = 1 + line_form.currency_id = self.company.currency_id + invoice = move_form.save() + self.assertFalse(invoice.invoice_line_ids.agent_ids) + + def test_commission_propagation(self): + """Test propagation of agents from partner to invoice.""" + self.partner.agent_ids = [(4, self.agent_monthly.id)] + move_form = Form( + self.env["account.move"].with_context(default_move_type="out_invoice") + ) + move_form.partner_id = self.partner + with move_form.invoice_line_ids.new() as line_form: + line_form.currency_id = self.company.currency_id + line_form.product_id = self.product + line_form.quantity = 1 + invoice = move_form.save() + agent = invoice.invoice_line_ids.agent_ids + self._check_propagation(agent, self.commission_net_invoice, self.agent_monthly) + # Check agent change + agent.agent_id = self.agent_quaterly + self.assertTrue(agent.commission_id, self.commission_section_invoice) + # Check recomputation + agent.unlink() + invoice.recompute_lines_agents() + agent = invoice.invoice_line_ids.agent_ids + self._check_propagation(agent, self.commission_net_invoice, self.agent_monthly) + + def test_negative_settlements(self): + self.product.write({"list_price": 1000}) + agent = self.agent_monthly + commission = self.commission_net_invoice + invoice = self._process_invoice_and_settle(agent, commission, 1) + settlement = self.settle_model.search([("agent_id", "=", agent.id)]) + self.assertEqual(1, len(settlement)) + self.assertEqual(settlement.state, "settled") + commission_invoice = settlement.make_invoices( + product=self.commission_product, journal=self.journal + ) + self.assertEqual(settlement.state, "invoiced") + self.assertEqual(commission_invoice.move_type, "in_invoice") + refund = invoice._reverse_moves( + default_values_list=[{"invoice_date": invoice.invoice_date}], + ) + self.assertEqual( + invoice.invoice_line_ids.agent_ids.agent_id, + refund.invoice_line_ids.agent_ids.agent_id, + ) + refund.invoice_line_ids.agent_ids._compute_amount() + refund.action_post() + self._settle_agent_invoice(agent, 1) + settlements = self.settle_model.search([("agent_id", "=", agent.id)]) + self.assertEqual(2, len(settlements)) + second_settlement = settlements.filtered(lambda r: r.total < 0) + self.assertEqual(second_settlement.state, "settled") + # Use invoice wizard for testing also this part + wizard = self.env["commission.make.invoice"].create( + {"product_id": self.commission_product.id} + ) + action = wizard.button_create() + commission_refund = self.env["account.move"].browse(action["domain"][0][2]) + self.assertEqual(second_settlement.state, "invoiced") + self.assertEqual(commission_refund.move_type, "in_refund") + # Undo invoices + make invoice again to get a unified invoice + commission_invoices = commission_invoice + commission_refund + commission_invoices.button_cancel() + self.assertEqual(settlement.state, "except_invoice") + self.assertEqual(second_settlement.state, "except_invoice") + commission_invoices.unlink() + settlements.unlink() + self._settle_agent_invoice(False, 1) # agent=False for testing default + settlement = self.settle_model.search([("agent_id", "=", agent.id)]) + # Check make invoice wizard + action = settlement.action_invoice() + self.assertEqual(action["context"]["settlement_ids"], settlement.ids) + # Use invoice wizard for testing also this part + wizard = self.env["commission.make.invoice"].create( + { + "product_id": self.commission_product.id, + "journal_id": self.journal.id, + "settlement_ids": [(4, settlement.id)], + } + ) + action = wizard.button_create() + invoice = self.env["account.move"].browse(action["domain"][0][2]) + self.assertEqual(invoice.move_type, "in_invoice") + self.assertAlmostEqual(invoice.amount_total, 0) + + def test_negative_settlements_join_invoice(self): + self.product.write({"list_price": 1000}) + agent = self.agent_monthly + commission = self.commission_net_invoice + invoice = self._process_invoice_and_settle(agent, commission, 1) + settlement = self.settle_model.search([("agent_id", "=", agent.id)]) + self.assertEqual(1, len(settlement)) + self.assertEqual(settlement.state, "settled") + refund = invoice._reverse_moves( + default_values_list=[ + { + "invoice_date": invoice.invoice_date + relativedelta(months=-1), + "date": invoice.date + relativedelta(months=-1), + } + ], + ) + self.assertEqual( + invoice.invoice_line_ids.agent_ids.agent_id, + refund.invoice_line_ids.agent_ids.agent_id, + ) + refund.action_post() + self._settle_agent_invoice(agent, 1) + settlements = self.settle_model.search([("agent_id", "=", agent.id)]) + self.assertEqual(2, len(settlements)) + second_settlement = settlements.filtered(lambda r: r.total < 0) + self.assertEqual(second_settlement.state, "settled") + # Use invoice wizard for testing also this part + wizard = self.env["commission.make.invoice"].create( + {"product_id": self.commission_product.id, "grouped": True} + ) + action = wizard.button_create() + commission_invoice = self.env["account.move"].browse(action["domain"][0][2]) + self.assertEqual(1, len(commission_invoice)) + self.assertEqual(commission_invoice.move_type, "in_invoice") + self.assertAlmostEqual(commission_invoice.amount_total, 0, places=2) + + def _create_multi_settlements(self): + agent = self.agent_monthly + commission = self.commission_section_invoice + today = fields.Date.today() + last_month = today + relativedelta(months=-1) + invoice_1 = self._create_invoice(agent, commission, today) + invoice_1.action_post() + invoice_2 = self._create_invoice(agent, commission, last_month) + invoice_2.action_post() + self._settle_agent_invoice(agent, 1) + settlements = self.settle_model.search( + [ + ("agent_id", "=", agent.id), + ("state", "=", "settled"), + ] + ) + self.assertEqual(2, len(settlements)) + return settlements + + def test_commission_single_invoice(self): + settlements = self._create_multi_settlements() + settlements.make_invoices(self.journal, self.commission_product, grouped=True) + invoices = settlements.mapped("invoice_id") + self.assertEqual(1, len(invoices)) + + def test_commission_multiple_invoice(self): + settlements = self._create_multi_settlements() + settlements.make_invoices(self.journal, self.commission_product) + invoices = settlements.mapped("invoice_id") + self.assertEqual(2, len(invoices)) + + def test_biweekly(self): + agent = self.agent_biweekly + commission = self.commission_net_invoice + invoice = self._create_invoice(agent, commission) + invoice.invoice_date = "2022-01-01" + invoice.date = "2022-01-01" + invoice.action_post() + invoice2 = self._create_invoice(agent, commission, date="2022-01-16") + invoice2.invoice_date = "2022-01-16" + invoice2.date = "2022-01-16" + invoice2.action_post() + invoice3 = self._create_invoice(agent, commission, date="2022-01-31") + invoice3.invoice_date = "2022-01-31" + invoice3.date = "2022-01-31" + invoice3.action_post() + self._settle_agent_invoice(agent=self.agent_biweekly, date="2022-02-01") + settlements = self.settle_model.search( + [("agent_id", "=", self.agent_biweekly.id)] + ) + self.assertEqual(len(settlements), 2) diff --git a/account_commission/views/account_commission_settlement_view.xml b/account_commission/views/account_commission_settlement_view.xml new file mode 100644 index 000000000..eb789f6ae --- /dev/null +++ b/account_commission/views/account_commission_settlement_view.xml @@ -0,0 +1,32 @@ + + + + Settlements + commission.settlement + + + + + + + + + Settlement lines + commission.settlement.line + + + + + + + + diff --git a/account_commission/views/account_move_views.xml b/account_commission/views/account_move_views.xml new file mode 100644 index 000000000..5b6ceeccb --- /dev/null +++ b/account_commission/views/account_move_views.xml @@ -0,0 +1,108 @@ + + + + + account.invoice.line.agent + + + + + + + + + + account.move.line + + + + + + + + + + + + + account.invoice.form.agent + account.move + + + + + +