diff --git a/hr_commission/__manifest__.py b/hr_commission/__manifest__.py index 422f75ef..8db1f5a6 100644 --- a/hr_commission/__manifest__.py +++ b/hr_commission/__manifest__.py @@ -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..07b39f35 100644 --- a/hr_commission/models/account.py +++ b/hr_commission/models/account.py @@ -37,15 +37,32 @@ class AccountMove(models.Model): return res def amount_for_commission(self, commission=None): + # Override to exclude ineligible products + amount = 0.0 + invoice_lines = self.invoice_line_ids.filtered(lambda l: not l.product_id.is_commission_exempt) + sign = -1 if self.move_type in ['in_refund', 'out_refund'] else 1 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 + margin_threshold = float(self.env['ir.config_parameter'].sudo().get_param('commission.margin.threshold', 0.0)) + if margin_threshold: + invoice_lines = invoice_lines.filtered(lambda l: l.get_margin_percent() > margin_threshold) + amount = sum(invoice_lines.mapped('margin')) elif self.company_id.commission_amount_type == 'on_invoice_untaxed': - return self.amount_untaxed_signed - return self.amount_total_signed + amount = sum(invoice_lines.mapped('price_subtotal')) + else: + amount = sum(invoice_lines.mapped('price_total')) + return amount * sign 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' + + def get_margin_percent(self): + if not self.price_subtotal: + return 0.0 + return ((self.margin or 0.0) / self.price_subtotal) * 100.0 diff --git a/hr_commission/models/product_template.py b/hr_commission/models/product_template.py new file mode 100644 index 00000000..77d111c8 --- /dev/null +++ b/hr_commission/models/product_template.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_commission_exempt = fields.Boolean('Exclude from Commissions') 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..c28ae751 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_is_commission_exempt diff --git a/hr_commission/tests/test_is_commission_exempt.py b/hr_commission/tests/test_is_commission_exempt.py new file mode 100644 index 00000000..25e958e9 --- /dev/null +++ b/hr_commission/tests/test_is_commission_exempt.py @@ -0,0 +1,156 @@ +from odoo.tests import common +import logging + + +_logger = logging.getLogger(__name__) + + +class TestIsCommissionExempt(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_is_commission_exempt = self.env['product.product'].create({ + 'name': 'Test Product No Commission', + 'invoice_policy': 'order', + 'is_commission_exempt': 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_is_commission_exempt.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product_is_commission_exempt.uom_id.id, + 'price_unit': 20.0, + 'tax_id': False, + })] + }) + self.assertEqual(order.amount_total, 120.0) + return order + + def test_00_is_commission_exempt_total(self): + # TODO: test refunds + + # New attribute + self.assertFalse(self.product.is_commission_exempt) + self.assertTrue(self.product_is_commission_exempt.is_commission_exempt) + + # 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_is_commission_exempt_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_is_commission_exempt.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(lambda l: l.get_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_is_commission_exempt.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(lambda l: l.get_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..45ed86fe --- /dev/null +++ b/hr_commission/views/product_views.xml @@ -0,0 +1,30 @@ + + + + + product.template.common.form.inherit + product.template + + + + + + + + + + + + product.template.common.form.inherit.manager + product.template + + + + + 0 + + + + + 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 @@