From a0924cc03639e02bf2af20af89de56c9513c460f Mon Sep 17 00:00:00 2001 From: galeshibou Date: Thu, 29 Sep 2022 02:06:14 +0000 Subject: [PATCH] [IMP] hr_commission: margin threshold and no commission products H11044 --- hr_commission/__manifest__.py | 3 +- hr_commission/models/__init__.py | 1 + hr_commission/models/account.py | 42 ++++- hr_commission/models/product_template.py | 13 ++ hr_commission/models/res_config_settings.py | 14 +- hr_commission/tests/__init__.py | 1 + hr_commission/tests/test_no_commission.py | 156 ++++++++++++++++++ hr_commission/views/product_views.xml | 16 ++ .../views/res_config_settings_views.xml | 4 + 9 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 hr_commission/models/product_template.py create mode 100644 hr_commission/tests/test_no_commission.py create mode 100644 hr_commission/views/product_views.xml diff --git a/hr_commission/__manifest__.py b/hr_commission/__manifest__.py index 422f75ef..c467fd8c 100644 --- a/hr_commission/__manifest__.py +++ b/hr_commission/__manifest__.py @@ -8,7 +8,7 @@ 'license': 'OPL-1', 'website': 'https://hibou.io/', 'depends': [ - # 'account_invoice_margin', # optional + 'account_invoice_margin', 'account', 'hr_contract', ], @@ -19,6 +19,7 @@ 'views/commission_views.xml', 'views/hr_views.xml', 'views/partner_views.xml', + 'views/product_views.xml', 'views/res_config_settings_views.xml', ], 'installable': True, diff --git a/hr_commission/models/__init__.py b/hr_commission/models/__init__.py index d739b6ed..7573150d 100755 --- a/hr_commission/models/__init__.py +++ b/hr_commission/models/__init__.py @@ -4,5 +4,6 @@ from . import account from . import commission from . import hr from . import partner +from . import product_template from . import res_company from . import res_config_settings diff --git a/hr_commission/models/account.py b/hr_commission/models/account.py index 1048faa1..45bff231 100644 --- a/hr_commission/models/account.py +++ b/hr_commission/models/account.py @@ -37,15 +37,45 @@ class AccountMove(models.Model): return res def amount_for_commission(self, commission=None): - if hasattr(self, 'margin') and self.company_id.commission_amount_type == 'on_invoice_margin': - sign = -1 if self.move_type in ['in_refund', 'out_refund'] else 1 - return self.margin * sign - elif self.company_id.commission_amount_type == 'on_invoice_untaxed': - return self.amount_untaxed_signed - return self.amount_total_signed + # Override to exclude ineligible products + amount = 0.0 + if self.is_invoice(): + invoice_lines = self.invoice_line_ids.filtered(lambda l: not l.product_id.no_commission) + if self.company_id.commission_amount_type == 'on_invoice_margin': + margin_threshold = float(self.env['ir.config_parameter'].sudo().get_param('commission.margin.threshold', default=0.0)) + if margin_threshold: + invoice_lines = invoice_lines.filtered(lambda l: l.margin_percent > margin_threshold) + sign = -1 if self.move_type in ['in_refund', 'out_refund'] else 1 + margin = sum(invoice_lines.mapped('margin')) + amount = margin * sign + else: + amount = sum(invoice_lines.mapped('balance')) + amount = abs(amount) if self.move_type == 'entry' else -amount + return amount def action_cancel(self): res = super(AccountMove, self).action_cancel() for move in self: move.sudo().commission_ids.unlink() return res + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + margin_percent = fields.Float(string='Margin percent (%)', compute='_compute_margin_percent', digits=(3, 2)) + + @api.depends('margin', 'product_id', 'purchase_price', 'quantity', 'price_unit', 'price_subtotal') + def _compute_margin_percent(self): + for line in self: + currency = line.move_id.currency_id + price = line.purchase_price + if line.product_id and not price: + date = line.move_id.date if line.move_id.date else fields.Date.context_today(line.move_id) + from_cur = line.move_id.company_currency_id.with_context(date=date) + price = from_cur._convert(line.product_id.standard_price, currency, line.company_id, date, round=False) + total_price = price * line.quantity + if total_price == 0.0: + line.margin_percent = -1.0 + else: + line.margin_percent = (line.margin / total_price) * 100.0 diff --git a/hr_commission/models/product_template.py b/hr_commission/models/product_template.py new file mode 100644 index 00000000..9a8ca77e --- /dev/null +++ b/hr_commission/models/product_template.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + no_commission = fields.Boolean('Exclude from Commissions') + can_edit_no_commission = fields.Boolean(compute='_compute_can_edit_no_commission') + + def _compute_can_edit_no_commission(self): + can_edit = self.env.user.has_group('account.group_account_user') + for template in self: + template.can_edit_no_commission = can_edit diff --git a/hr_commission/models/res_config_settings.py b/hr_commission/models/res_config_settings.py index d09b5b38..4a8f9608 100644 --- a/hr_commission/models/res_config_settings.py +++ b/hr_commission/models/res_config_settings.py @@ -1,6 +1,6 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class ResConfigSettings(models.TransientModel): @@ -10,3 +10,15 @@ class ResConfigSettings(models.TransientModel): commission_liability_id = fields.Many2one(related='company_id.commission_liability_id', readonly=False) commission_type = fields.Selection(related='company_id.commission_type', readonly=False) commission_amount_type = fields.Selection(related='company_id.commission_amount_type', readonly=False) + commission_margin_threshold = fields.Float() + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + margin_threshold = float(self.env['ir.config_parameter'].sudo().get_param('commission.margin.threshold', default=0.0)) + res.update(commission_margin_threshold=margin_threshold) + return res + + def set_values(self): + super(ResConfigSettings, self).set_values() + self.env['ir.config_parameter'].sudo().set_param('commission.margin.threshold', self.commission_margin_threshold) diff --git a/hr_commission/tests/__init__.py b/hr_commission/tests/__init__.py index 4745f69f..2a5a8ba1 100755 --- a/hr_commission/tests/__init__.py +++ b/hr_commission/tests/__init__.py @@ -1,3 +1,4 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from . import test_commission +from . import test_no_commission diff --git a/hr_commission/tests/test_no_commission.py b/hr_commission/tests/test_no_commission.py new file mode 100644 index 00000000..b39cb09b --- /dev/null +++ b/hr_commission/tests/test_no_commission.py @@ -0,0 +1,156 @@ +from odoo.tests import common +import logging + + +_logger = logging.getLogger(__name__) + + +class TestNoCommission(common.TransactionCase): + def setUp(self): + super().setUp() + + # find and configure company commissions journal + expense_user_type = self.env['account.account.type'].search([('name', '=', 'Expenses')], limit=1) + self.assertTrue(expense_user_type) + expense_account = self.env['account.account'].search([('user_type_id', '=', expense_user_type.id), + ('company_id', '=', self.env.user.company_id.id)], limit=1) + self.assertTrue(expense_account) + commission_journal = self.env['account.journal'].search([ + ('type', '=', 'general'), + ('company_id', '=', expense_account.company_id.id), + ], limit=1) + self.assertTrue(commission_journal) + commission_journal.default_account_id = expense_account + commission_journal.default_account_id = expense_account + self.env.user.company_id.commission_journal_id = commission_journal + self.env.user.company_id.commission_type = 'on_invoice' + + self.sales_user = self.browse_ref('base.user_demo') + self.customer_partner = self.browse_ref('base.res_partner_12') + + self.sales_employee = self.sales_user.employee_id + self.sales_employee.write({ + 'address_home_id': self.sales_user.partner_id, + 'contract_ids': [(0, 0, { + 'date_start': '2016-01-01', + 'date_end': '2030-12-31', + 'name': 'Contract for tests', + 'wage': 1000.0, + # 'type_id': self.ref('hr_contract.hr_contract_type_emp'), + 'employee_id': self.sales_employee.id, + 'resource_calendar_id': self.ref('resource.resource_calendar_std'), + 'commission_rate': 10.0, + 'state': 'open', # if not "Running" then no automatic selection when Payslip is created in 11.0 + })], + }) + + self.product = self.env['product.product'].create({ + 'name': 'Test Product', + 'invoice_policy': 'order', + 'taxes_id': [], + }) + self.product_no_commission = self.env['product.product'].create({ + 'name': 'Test Product No Commission', + 'invoice_policy': 'order', + 'no_commission': True, + 'taxes_id': [], + }) + + def _createSaleOrder(self): + order = self.env['sale.order'].create({ + 'partner_id': self.customer_partner.id, + 'user_id': self.sales_user.id, + 'order_line': [(0, 0, { + 'name': 'test product', + 'product_id': self.product.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product.uom_id.id, + 'price_unit': 100.0, + 'tax_id': False, + }), (0, 0, { + 'name': 'test product no commission', + 'product_id': self.product_no_commission.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product_no_commission.uom_id.id, + 'price_unit': 20.0, + 'tax_id': False, + })] + }) + self.assertEqual(order.amount_total, 120.0) + return order + + def test_00_no_commission_total(self): + # TODO: test refunds + + # New attribute + self.assertFalse(self.product.no_commission) + self.assertTrue(self.product_no_commission.no_commission) + + # Calculate commission based on invoice total + self.env.user.company_id.commission_amount_type = 'on_invoice_total' + + sale = self._createSaleOrder() + sale.action_confirm() + self.assertIn(sale.state, ('sale', 'done'), 'Could not confirm, maybe archive exception rules.') + inv = sale._create_invoices() + self.assertFalse(inv.commission_ids, 'Commissions exist when invoice is created.') + inv.action_post() + self.assertTrue(inv.commission_ids, 'Commissions not created when invoice is validated.') + self.assertEqual(inv.amount_total, 120.0) + + user_commission = inv.commission_ids.filtered(lambda c: c.employee_id.id == self.sales_employee.id) + self.assertEqual(len(user_commission), 1) + + # rate = 10.0, total = 120.0, commission total = 100.0 + # commmission should be 10.0 + self.assertEqual(user_commission.amount, 10.0) + + def test_10_no_commission_margin(self): + self.env['ir.config_parameter'].set_param('commission.margin.threshold', '51.0') + low_margin_product = self.env['product.product'].create({ + 'name': 'Test Low Margin Product', + 'standard_price': 100.0, + 'invoice_policy': 'order', + }) + self.env.user.company_id.commission_amount_type = 'on_invoice_margin' + self.product.standard_price = 50.0 # margin is 100%, margin = $50.0 + self.product_no_commission.standard_price = 10.0 # margin is 100% + + sale = self._createSaleOrder() + sale.write({ + 'order_line': [(0, 0, { + 'name': 'test low margin product', + 'product_id': low_margin_product.id, + 'product_uom_qty': 1.0, + 'product_uom': low_margin_product.uom_id.id, + 'price_unit': 101.0, # margin is 1.0 % + 'tax_id': False, + })], + }) + + # Total margin is now $61.0, but eligible margin should still be $50.0 + sale.action_confirm() + self.assertIn(sale.state, ('sale', 'done'), 'Could not confirm, maybe archive exception rules.') + inv = sale._create_invoices() + self.assertEqual(inv.invoice_line_ids.mapped('margin_percent'), [100.0, 100.0, 1.0]) + self.assertFalse(inv.commission_ids, 'Commissions exist when invoice is created.') + inv.action_post() + self.assertTrue(inv.commission_ids, 'Commissions not created when invoice is validated.') + + user_commission = inv.commission_ids.filtered(lambda c: c.employee_id.id == self.sales_employee.id) + self.assertEqual(len(user_commission), 1) + + # rate = 10.0, total margin = 60.0, commission margin = 50.0 + # commission should be 5.0 + self.assertEqual(user_commission.amount, 5.0) + + def test_20_test_zero_price(self): + self.env.user.company_id.commission_amount_type = 'on_invoice_margin' + self.product.standard_price = 0.0 # margin_percent is NaN + self.product_no_commission.standard_price = 10.0 # margin is 100% + + sale = self._createSaleOrder() + sale.action_confirm() + self.assertIn(sale.state, ('sale', 'done'), 'Could not confirm, maybe archive exception rules.') + inv = sale._create_invoices() + self.assertEqual(inv.invoice_line_ids.mapped('margin_percent'), [-1.0, 100.0]) diff --git a/hr_commission/views/product_views.xml b/hr_commission/views/product_views.xml new file mode 100644 index 00000000..29f344ad --- /dev/null +++ b/hr_commission/views/product_views.xml @@ -0,0 +1,16 @@ + + + + + product.template.common.form.inherit + product.template + + + + + + + + + + diff --git a/hr_commission/views/res_config_settings_views.xml b/hr_commission/views/res_config_settings_views.xml index ccb41ddd..555f5faa 100644 --- a/hr_commission/views/res_config_settings_views.xml +++ b/hr_commission/views/res_config_settings_views.xml @@ -35,6 +35,10 @@