Ecocentral/ecocentral#152: include in_refund invoices. rappel_pending readonly. vendor ref in invoice lines

This commit is contained in:
Luis 2026-06-23 12:16:47 +02:00
parent 4c07dd042d
commit 29ef56b800
5 changed files with 140 additions and 18 deletions

View file

@ -16,13 +16,16 @@ class AccountMove(models.Model):
def create(self, vals_list): def create(self, vals_list):
moves = super().create(vals_list) moves = super().create(vals_list)
for move in moves: 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 move.rappel_pending = True
return moves return moves
@api.onchange("partner_id") @api.onchange("partner_id")
def _onchange_partner_id_rappel(self): 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.rappel_pending = bool(
self.partner_id and self.partner_id.rappel_commission self.partner_id and self.partner_id.rappel_commission
) )

View file

@ -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( move = self.init_invoice(
"in_invoice", "in_invoice",
partner=partner, partner=partner,
@ -77,6 +77,22 @@ class TestPurchaseRappel(AccountTestInvoicingCommon):
post=False, post=False,
) )
move.invoice_line_ids[0].price_unit = amount 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: if post:
move.action_post() move.action_post()
return move return move
@ -236,3 +252,82 @@ class TestPurchaseRappel(AccountTestInvoicingCommon):
) )
with self.assertRaises(UserError): with self.assertRaises(UserError):
wizard.action_create_rappel_invoices() 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)

View file

@ -10,8 +10,9 @@
<field name="to_check" position="after"> <field name="to_check" position="after">
<field name="rappel_pending" <field name="rappel_pending"
optional="show" optional="show"
readonly="1"
widget="boolean_toggle" widget="boolean_toggle"
column_invisible="context.get('default_move_type') != 'in_invoice'"/> column_invisible="context.get('default_move_type') not in ('in_invoice', 'in_refund')"/>
</field> </field>
</field> </field>
</record> </record>
@ -40,7 +41,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="to_check" position="after"> <field name="to_check" position="after">
<field name="rappel_pending" <field name="rappel_pending"
invisible="move_type != 'in_invoice'" invisible="move_type not in ('in_invoice', 'in_refund')"
readonly="1"
widget="boolean_toggle"/> widget="boolean_toggle"/>
</field> </field>
</field> </field>

View file

@ -49,16 +49,17 @@ class AccountMoveRappelWizard(models.TransientModel):
active_ids = self.env.context.get("active_ids", []) active_ids = self.env.context.get("active_ids", [])
moves = self.env["account.move"].browse(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( 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: if not valid_moves:
raise UserError( raise UserError(
_( _(
"No posted vendor bills found in the selection. " "No posted vendor bills or refunds found in the selection. "
"Only posted (validated) vendor bills can be used to create " "Only posted (validated) vendor bills and refunds can be used to "
"rappel commission invoices." "create rappel commission invoices."
) )
) )
@ -135,14 +136,31 @@ class AccountMoveRappelWizard(models.TransientModel):
for bill in bills: for bill in bills:
taxable_base = bill.amount_untaxed taxable_base = bill.amount_untaxed
commission_amount = taxable_base * rate / 100.0 commission_amount = taxable_base * rate / 100.0
name = _( vendor_ref = bill.ref or bill.name
"Rappel commission %(rate)s%% — Bill %(bill)s " if bill.move_type == "in_refund":
"(taxable base: %(base)s %(currency)s)", # Vendor refunds reduce the commission: negate the amount so
rate=rate, # the resulting line subtracts from the rappel invoice total.
bill=bill.name, # amount_untaxed is always positive in Odoo, so we flip the
base=f"{taxable_base:,.2f}", # sign here and display a negative base in the description.
currency=bill.currency_id.name, 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( invoice_lines.append(
( (
0, 0,

View file

@ -24,6 +24,10 @@
<field name="move_ids"> <field name="move_ids">
<tree string="Vendor Bills" editable="0"> <tree string="Vendor Bills" editable="0">
<field name="name"/> <field name="name"/>
<field name="move_type"
widget="badge"
decoration-info="move_type == 'in_invoice'"
decoration-danger="move_type == 'in_refund'"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="invoice_date"/> <field name="invoice_date"/>
<field name="amount_untaxed" <field name="amount_untaxed"