diff --git a/purchase_rappel/models/account_move.py b/purchase_rappel/models/account_move.py index a9360a1..9160db2 100644 --- a/purchase_rappel/models/account_move.py +++ b/purchase_rappel/models/account_move.py @@ -16,13 +16,16 @@ class AccountMove(models.Model): def create(self, vals_list): moves = super().create(vals_list) for move in moves: - if move.move_type == "in_invoice" and move.partner_id.rappel_commission: + if ( + move.move_type in ("in_invoice", "in_refund") + and move.partner_id.rappel_commission + ): move.rappel_pending = True return moves @api.onchange("partner_id") def _onchange_partner_id_rappel(self): - if self.move_type == "in_invoice": + if self.move_type in ("in_invoice", "in_refund"): self.rappel_pending = bool( self.partner_id and self.partner_id.rappel_commission ) diff --git a/purchase_rappel/tests/test_purchase_rappel.py b/purchase_rappel/tests/test_purchase_rappel.py index 18cdb70..aa7568f 100644 --- a/purchase_rappel/tests/test_purchase_rappel.py +++ b/purchase_rappel/tests/test_purchase_rappel.py @@ -69,7 +69,7 @@ class TestPurchaseRappel(AccountTestInvoicingCommon): } ) - def _create_vendor_bill(self, partner, amount=1000.0, post=True): + def _create_vendor_bill(self, partner, amount=1000.0, post=True, ref=None): move = self.init_invoice( "in_invoice", partner=partner, @@ -77,6 +77,22 @@ class TestPurchaseRappel(AccountTestInvoicingCommon): post=False, ) move.invoice_line_ids[0].price_unit = amount + if ref is not None: + move.ref = ref + if post: + move.action_post() + return move + + def _create_vendor_refund(self, partner, amount=200.0, post=True, ref=None): + move = self.init_invoice( + "in_refund", + partner=partner, + products=self.product_a, + post=False, + ) + move.invoice_line_ids[0].price_unit = amount + if ref is not None: + move.ref = ref if post: move.action_post() return move @@ -236,3 +252,82 @@ class TestPurchaseRappel(AccountTestInvoicingCommon): ) with self.assertRaises(UserError): wizard.action_create_rappel_invoices() + + def test_wizard_vendor_ref_in_description(self): + bill_with_ref = self._create_vendor_bill( + self.vendor_rappel, amount=1000.0, ref="VENDOR-REF-001" + ) + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill_with_ref.ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + line_name = rappel_invoice.invoice_line_ids[0].name + self.assertIn("VENDOR-REF-001", line_name) + self.assertNotIn(bill_with_ref.name, line_name) + + bill_without_ref = self._create_vendor_bill( + self.vendor_rappel, amount=1000.0, ref=None + ) + wizard2 = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill_without_ref.ids) + .create({}) + ) + action2 = wizard2.action_create_rappel_invoices() + rappel_invoice2 = self.env["account.move"].browse(action2["res_id"]) + line_name2 = rappel_invoice2.invoice_line_ids[0].name + self.assertIn(bill_without_ref.name, line_name2) + + def test_wizard_includes_in_refund(self): + bill = self._create_vendor_bill(self.vendor_rappel, amount=1000.0) + refund = self._create_vendor_refund(self.vendor_rappel, amount=200.0) + self.assertTrue(refund.rappel_pending) + + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=(bill + refund).ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + + self.assertEqual(len(rappel_invoice.invoice_line_ids), 2) + amounts = sorted(rappel_invoice.invoice_line_ids.mapped("price_unit")) + self.assertEqual(amounts, [-10.0, 50.0]) + self.assertFalse(bill.rappel_pending) + self.assertFalse(refund.rappel_pending) + + def test_wizard_refund_subtracts_commission(self): + bill = self._create_vendor_bill(self.vendor_rappel, amount=1000.0) + refund = self._create_vendor_refund(self.vendor_rappel, amount=200.0) + + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=(bill + refund).ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + + self.assertEqual(len(rappel_invoice.invoice_line_ids), 2) + line_by_sign = { + line.price_unit > 0: line for line in rappel_invoice.invoice_line_ids + } + self.assertEqual(line_by_sign[True].price_unit, 50.0) + self.assertEqual(line_by_sign[False].price_unit, -10.0) + self.assertEqual(rappel_invoice.amount_untaxed, 40.0) + self.assertIn("Refund", line_by_sign[False].name) + + def test_rappel_pending_set_on_refund_create(self): + refund = self._create_vendor_refund( + self.vendor_rappel, amount=200.0, post=False + ) + self.assertTrue(refund.rappel_pending) + + refund_no_rappel = self._create_vendor_refund( + self.vendor_no_rappel, amount=200.0, post=False + ) + self.assertFalse(refund_no_rappel.rappel_pending) diff --git a/purchase_rappel/views/account_move_views.xml b/purchase_rappel/views/account_move_views.xml index 9eb9496..5846466 100644 --- a/purchase_rappel/views/account_move_views.xml +++ b/purchase_rappel/views/account_move_views.xml @@ -10,8 +10,9 @@ + column_invisible="context.get('default_move_type') not in ('in_invoice', 'in_refund')"/> @@ -40,7 +41,8 @@ diff --git a/purchase_rappel/wizard/account_move_rappel_wizard.py b/purchase_rappel/wizard/account_move_rappel_wizard.py index 078f7f1..1faede7 100644 --- a/purchase_rappel/wizard/account_move_rappel_wizard.py +++ b/purchase_rappel/wizard/account_move_rappel_wizard.py @@ -49,16 +49,17 @@ class AccountMoveRappelWizard(models.TransientModel): active_ids = self.env.context.get("active_ids", []) moves = self.env["account.move"].browse(active_ids) - # Filter only posted vendor bills + # Filter only posted vendor bills and refunds valid_moves = moves.filtered( - lambda m: m.move_type == "in_invoice" and m.state == "posted" + lambda m: m.move_type in ("in_invoice", "in_refund") + and m.state == "posted" ) if not valid_moves: raise UserError( _( - "No posted vendor bills found in the selection. " - "Only posted (validated) vendor bills can be used to create " - "rappel commission invoices." + "No posted vendor bills or refunds found in the selection. " + "Only posted (validated) vendor bills and refunds can be used to " + "create rappel commission invoices." ) ) @@ -135,14 +136,31 @@ class AccountMoveRappelWizard(models.TransientModel): for bill in bills: taxable_base = bill.amount_untaxed commission_amount = taxable_base * rate / 100.0 - name = _( - "Rappel commission %(rate)s%% — Bill %(bill)s " - "(taxable base: %(base)s %(currency)s)", - rate=rate, - bill=bill.name, - base=f"{taxable_base:,.2f}", - currency=bill.currency_id.name, - ) + vendor_ref = bill.ref or bill.name + if bill.move_type == "in_refund": + # Vendor refunds reduce the commission: negate the amount so + # the resulting line subtracts from the rappel invoice total. + # amount_untaxed is always positive in Odoo, so we flip the + # sign here and display a negative base in the description. + commission_amount = -commission_amount + display_base = -taxable_base + name = _( + "Rappel commission %(rate)s%% — Refund %(bill)s " + "(taxable base: %(base)s %(currency)s)", + rate=rate, + bill=vendor_ref, + base=f"{display_base:,.2f}", + currency=bill.currency_id.name, + ) + else: + name = _( + "Rappel commission %(rate)s%% — Bill %(bill)s " + "(taxable base: %(base)s %(currency)s)", + rate=rate, + bill=vendor_ref, + base=f"{taxable_base:,.2f}", + currency=bill.currency_id.name, + ) invoice_lines.append( ( 0, diff --git a/purchase_rappel/wizard/account_move_rappel_wizard_views.xml b/purchase_rappel/wizard/account_move_rappel_wizard_views.xml index 3ce46a9..3aa0cbf 100644 --- a/purchase_rappel/wizard/account_move_rappel_wizard_views.xml +++ b/purchase_rappel/wizard/account_move_rappel_wizard_views.xml @@ -24,6 +24,10 @@ +