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 @@
+