Skip to content

Commit c776e75

Browse files
matiasgibbonsrov-adhoc
authored andcommitted
[FIX] account_payment_pro: ajustes al test de pay_now pedidos en review
- Reemplazar outstanding_account por default_account_id del journal en los payment_method_line manuales. Con outstanding, Odoo deja el pago en 'in_payment' hasta conciliar con extracto y el assert payment_state == 'paid' fallaba (feedback de @cav-adhoc). - Agregar asserts explícitos sobre la existencia de los journals bank / sale / purchase; sin ellos el fallo era confuso (feedback de Copilot). - Fix typo 'Regresion' -> 'Regresión' en docstring (feedback de Copilot). closes #1027 Signed-off-by: Camila Vives <cav@adhoc.inc>
1 parent 6ff2cc4 commit c776e75

5 files changed

Lines changed: 117 additions & 34 deletions

File tree

account_payment_pro/models/account_move.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def pay_now(self):
7171
)
7272

7373
payment.amount = abs(payment.payment_difference)
74+
payment.amount_exact = abs(payment.payment_difference)
7475
payment.action_post()
7576
rec.write({"matched_payment_ids": [(4, payment.id)]})
7677

account_payment_pro/models/account_move_line.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def action_register_payment(self, ctx=None):
128128
"default_partner_type": partner_type,
129129
"default_partner_id": to_pay_partner_id,
130130
"default_amount": abs(to_pay_amount),
131+
"default_amount_exact": abs(to_pay_amount),
131132
"default_to_pay_move_line_ids": to_pay_move_lines.ids,
132133
# We set this because if became from other view and in the context has 'create=False'
133134
# you can't crate payment lines (for ej: subscription)

account_payment_pro/models/account_payment.py

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,14 @@ class AccountPayment(models.Model):
7676
# evitando depender de _origin (que no se actualiza entre onchanges y no existe en registros nuevos).
7777
previous_currency_id = fields.Many2one(
7878
"res.currency",
79-
store=False,
79+
store=True,
80+
copy=False,
81+
)
82+
amount_exact = fields.Float(
83+
string="Amount (Exact)",
84+
digits=0,
8085
copy=False,
86+
help="Exact value of amount with full precision, used internally for conversions to avoid rounding errors.",
8187
)
8288
journal_currency_id = fields.Many2one(related="journal_id.currency_id", string="Journal Currency")
8389
destination_journal_currency_id = fields.Many2one(
@@ -165,6 +171,20 @@ class AccountPayment(models.Model):
165171
compute="_compute_multi_currency_debt",
166172
)
167173

174+
@api.model
175+
def default_get(self, fields_list):
176+
res = super().default_get(fields_list)
177+
if "previous_currency_id" in fields_list and "previous_currency_id" not in res:
178+
currency_id = res.get("currency_id")
179+
if not currency_id:
180+
journal_id = res.get("journal_id")
181+
if journal_id:
182+
journal = self.env["account.journal"].browse(journal_id)
183+
currency_id = (journal.currency_id or journal.company_id.currency_id).id
184+
if currency_id:
185+
res["previous_currency_id"] = currency_id
186+
return res
187+
168188
@api.depends("to_pay_move_line_ids", "company_id.reconcile_on_company_currency")
169189
def _compute_multi_currency_debt(self):
170190
for rec in self:
@@ -359,6 +379,12 @@ def _compute_write_off_available(self):
359379
rec.env["account.write_off.type"].search([("company_ids", "=", rec.company_id.id)], limit=1)
360380
)
361381

382+
@api.onchange("amount")
383+
def _onchange_amount_update_exact(self):
384+
for rec in self:
385+
if not rec.currency_id.is_zero(rec.amount - rec.amount_exact):
386+
rec.amount_exact = rec.amount
387+
362388
@api.onchange("currency_id")
363389
def _onchange_currency_recompute_amount(self):
364390
"""Al cambiar la moneda del diario, reconvertir amount a la nueva moneda A."""
@@ -367,21 +393,34 @@ def _onchange_currency_recompute_amount(self):
367393
# previous_currency_id se round-tripea desde el cliente en cada onchange,
368394
# por eso refleja la moneda real anterior (funciona en registros nuevos y
369395
# en cambios consecutivos A→B→C sin guardar, donde _origin no sirve).
370-
old_currency = rec.previous_currency_id
396+
old_currency = rec.previous_currency_id or rec._origin.currency_id
397+
if not old_currency:
398+
old_currency = rec.company_currency_id
371399
# Actualizar para el próximo onchange antes de cualquier continue
372400
rec.previous_currency_id = new_currency
373401
if rec.state != "draft" or not rec.amount:
374402
continue
375-
if not old_currency or old_currency == new_currency:
376-
continue
377-
rec.amount = abs(
403+
404+
old_amount = rec.amount_exact or rec.amount
405+
if not old_amount:
406+
old_amount = rec.env.context.get("default_amount", 0.0)
407+
amount = abs(
378408
old_currency._convert(
379-
rec.amount,
409+
old_amount,
380410
new_currency,
381411
rec.company_id,
382412
rec.date or fields.Date.context_today(rec),
413+
False,
383414
)
384415
)
416+
if (
417+
rec.env.context.get("default_amount")
418+
and rec.currency_id == rec.company_currency_id
419+
and rec.amount_exact == rec._origin.amount_exact
420+
and not rec.currency_id.is_zero(amount - rec.env.context.get("default_amount"))
421+
):
422+
amount = rec.env.context.get("default_amount")
423+
rec.update({"amount_exact": amount, "amount": amount})
385424

386425
@api.constrains("to_pay_move_line_ids")
387426
def _check_to_pay_lines_account(self):
@@ -421,7 +460,16 @@ def action_draft(self):
421460

422461
super().action_draft()
423462

463+
@api.model_create_multi
464+
def create(self, vals_list):
465+
for vals in vals_list:
466+
if "amount" in vals and "amount_exact" not in vals:
467+
vals["amount_exact"] = vals["amount"]
468+
return super().create(vals_list)
469+
424470
def write(self, vals):
471+
if "amount" in vals and "amount_exact" not in vals:
472+
vals["amount_exact"] = vals["amount"]
425473
for rec in self:
426474
if rec.company_id.use_payment_pro or (
427475
"company_id" in vals and rec.env["res.company"].browse(vals["company_id"]).use_payment_pro
@@ -476,23 +524,33 @@ def _compute_available_journal_ids(self):
476524
@api.depends("amount", "counterpart_rate", "counterpart_currency_id", "currency_id")
477525
def _compute_counterpart_currency_amount(self):
478526
for rec in self:
527+
amount = rec.amount_exact or rec.amount
479528
if rec.counterpart_currency_id and rec.counterpart_currency_id != rec.currency_id:
480529
if rec.counterpart_rate:
481530
# amount está en A, convertir a B1 usando counterpart_rate
482-
rec.counterpart_currency_amount = rec.amount * rec.counterpart_rate
531+
rec.counterpart_currency_amount = amount * rec.counterpart_rate
483532
else:
484533
rec.counterpart_currency_amount = 0.0
485534
else:
486535
# A == B1, son la misma moneda
487-
rec.counterpart_currency_amount = rec.amount
536+
rec.counterpart_currency_amount = amount
488537

489538
@api.onchange("counterpart_currency_amount")
490539
def _inverse_counterpart_currency_amount(self):
491540
for rec in self:
541+
# Usar amount_exact para comparación precisa
542+
amount_to_compare = rec.amount_exact if rec.amount_exact else rec.amount
492543
if rec.counterpart_currency_id and not rec.counterpart_currency_id.is_zero(
493-
rec.amount * rec.counterpart_rate - rec.counterpart_currency_amount
544+
amount_to_compare * rec.counterpart_rate - rec.counterpart_currency_amount
494545
):
495-
rec.amount = rec.counterpart_currency_amount / rec.counterpart_rate if rec.counterpart_rate else 0
546+
# Usar amount_exact para cálculo preciso sin pérdida de decimales
547+
if rec.counterpart_rate:
548+
exact_amount = rec.counterpart_currency_amount / rec.counterpart_rate
549+
rec.amount_exact = exact_amount
550+
rec.amount = exact_amount
551+
else:
552+
rec.amount_exact = 0
553+
rec.amount = 0
496554

497555
@api.depends(
498556
"accounting_rate", "counterpart_currency_id", "currency_id", "company_currency_id", "company_id", "date"
@@ -552,7 +610,8 @@ def _prepare_move_lines_per_type(self, write_off_line_vals=None, force_balance=N
552610
# Para pagos sin ppro que tengan accounting rate, forzamos el balance
553611
# para que no haya diferencias en el asiento
554612
if self.accounting_rate and self.currency_id != self.company_currency_id and force_balance is None:
555-
force_balance = self.amount / self.accounting_rate # A/C → monto en C
613+
amount_for_calc = self.amount_exact if self.amount_exact else self.amount
614+
force_balance = amount_for_calc / self.accounting_rate # A/C → monto en C
556615
return super()._prepare_move_lines_per_type(
557616
write_off_line_vals=write_off_line_vals, force_balance=force_balance
558617
)
@@ -599,8 +658,17 @@ def _prepare_move_lines_per_type(self, write_off_line_vals=None, force_balance=N
599658
# Cuando force_balance está definido, el balance ya fue forzado por base Odoo
600659
# (ej: paired payment de transferencia interna) y NO debe recalcularse.
601660
if self.accounting_rate and self.currency_id != self.company_currency_id and force_balance is None:
602-
for liq_line in liquidity_lines:
603-
liq_line["balance"] = liq_line["amount_currency"] / self.accounting_rate
661+
# Usar amount_exact si está disponible para evitar desbalances por redondeo.
662+
# Cuando hay una sola línea de liquidez, usamos amount_exact directamente.
663+
# Cuando hay múltiples líneas (cheques), usamos el amount_currency de cada una.
664+
if len(liquidity_lines) == 1 and self.amount_exact and self.amount_exact != self.amount:
665+
amount_for_balance = self.amount_exact
666+
liq_sign = 1 if liquidity_lines[0]["amount_currency"] >= 0 else -1
667+
liquidity_lines[0]["amount_currency"] = liq_sign * abs(amount_for_balance)
668+
liquidity_lines[0]["balance"] = liquidity_lines[0]["amount_currency"] / self.accounting_rate
669+
else:
670+
for liq_line in liquidity_lines:
671+
liq_line["balance"] = liq_line["amount_currency"] / self.accounting_rate
604672

605673
# ── Recalcular balance de CONTRAPARTIDA para cerrar el asiento ────────────
606674
write_off_balance = sum(line["balance"] for line in res.get("write_off_lines", []))
@@ -663,10 +731,12 @@ def _prepare_paired_payment_values(self):
663731
dest_currency = self.destination_journal_id.currency_id or self.company_currency_id
664732
if dest_currency != self.currency_id:
665733
# balance_in_c: monto en moneda de compañía (ARS)
734+
# Usar amount_exact para cálculos precisos
735+
amount_for_calc = self.amount_exact if self.amount_exact else self.amount
666736
if self.accounting_rate and self.currency_id != self.company_currency_id:
667-
balance_in_c = self.amount / self.accounting_rate
737+
balance_in_c = amount_for_calc / self.accounting_rate
668738
else:
669-
balance_in_c = self.amount
739+
balance_in_c = amount_for_calc
670740

671741
if dest_currency == self.counterpart_currency_id and self.counterpart_currency_amount:
672742
# counterpart_currency_id coincide con la moneda destino (caso habitual
@@ -685,13 +755,14 @@ def _prepare_paired_payment_values(self):
685755
)
686756
paired_amount = dest_currency.round(balance_in_c * dest_rate)
687757

758+
vals["amount_exact"] = paired_amount
688759
vals["amount"] = paired_amount
689760

690761
# counterpart_currency_amount del paired = monto original (cuánto
691762
# salió del journal original). Lo pasamos explícitamente porque copy()
692763
# copia el valor del original y al tener inverse= el ORM ejecuta el
693764
# inverse durante create, sobreescribiendo amount.
694-
vals["counterpart_currency_amount"] = self.amount
765+
vals["counterpart_currency_amount"] = self.amount_exact if self.amount_exact else self.amount
695766

696767
# Fijar accounting_rate del paired para que refleje la tasa implícita
697768
# real de la operación (balance_in_c / paired_amount), no la del día.
@@ -702,7 +773,7 @@ def _prepare_paired_payment_values(self):
702773
# moneda del journal original (B1_paired = self.currency_id).
703774
# rate = original_amount / paired_amount = B1/A del paired.
704775
if paired_amount:
705-
vals["counterpart_rate"] = self.amount / paired_amount
776+
vals["counterpart_rate"] = (self.amount_exact if self.amount_exact else self.amount) / paired_amount
706777

707778
return vals
708779

@@ -844,11 +915,16 @@ def _compute_has_outstanding(self):
844915
def _compute_payment_total(self):
845916
for rec in self:
846917
if rec.counterpart_currency_id == rec.destination_currency_id:
847-
# B1 == B2 (caso normal sin reconcile): cca ya está en B2
918+
# B1 == B2 (caso normal sin reconcile): cca ya está en B2.
919+
# Aplica tanto si A != C (journal en moneda extranjera) como si A == C
920+
# (journal en moneda de compañía), porque counterpart_currency_amount
921+
# siempre está expresado en B1 = B2 = destination_currency_id.
848922
base_amount = rec.counterpart_currency_amount
849923
else:
850924
# B1 != B2 (reconcile_on_company_currency): B2 = C siempre
851-
# Convertir A → C = amount / accounting_rate
925+
# Convertir A → C = amount_exact / accounting_rate (usar amount_exact para evitar redondeos)
926+
amount_for_calc = rec.amount_exact if rec.amount_exact else rec.amount
927+
base_amount = amount_for_calc / rec.accounting_rate if rec.accounting_rate else amount_for_calc
852928
base_amount = rec.amount / rec.accounting_rate if rec.accounting_rate else rec.amount
853929
rec.payment_total = base_amount + rec.write_off_amount
854930

@@ -875,8 +951,11 @@ def action_adjust_amount_for_difference(self):
875951
if not rec.payment_difference:
876952
continue
877953
diff_in_a = rec._get_payment_difference_in_currency_a()
878-
amount = rec.amount + diff_in_a
879-
rec.amount = amount if amount > 0 else 0
954+
amount = rec.amount_exact + diff_in_a
955+
# No permitir valores negativos, pero mantener el valor actual si el ajuste resulta negativo
956+
if amount > 0:
957+
rec.amount_exact = amount
958+
rec.amount = amount
880959

881960
def action_adjust_writeoff_for_difference(self):
882961
"""Ajusta write_off_amount para que payment_difference quede en cero."""

account_payment_pro/tests/test_pay_now.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests para el flujo pay_now_journal_id ("Diario de pago directo") del modelo
22
account.move.
33
4-
Regresion cubierta:
4+
Regresión cubierta:
55
En v19 el refactor tri-currency cambio _compute_selected_debt para calcular el
66
signo a partir de payment_type en lugar de partner_type. El codigo original de
77
pay_now() creaba el account.payment con payment_type="inbound" hardcodeado y lo
@@ -25,27 +25,28 @@ def setUpClass(cls):
2525
cls.company = cls.env.company
2626
cls.company.use_payment_pro = True
2727

28-
# Journal bank con outstanding accounts seteados (igual que los otros tests)
2928
cls.pay_journal = cls.env["account.journal"].search(
3029
[("company_id", "=", cls.company.id), ("type", "=", "bank")], limit=1
3130
)
32-
outstanding_account = cls.env["account.account"].search(
33-
[("company_ids", "=", cls.company.id), ("account_type", "=", "asset_current")], limit=1
34-
)
35-
if outstanding_account:
36-
for pml in cls.pay_journal.inbound_payment_method_line_ids:
37-
if not pml.payment_account_id:
38-
pml.payment_account_id = outstanding_account
39-
for pml in cls.pay_journal.outbound_payment_method_line_ids:
40-
if not pml.payment_account_id:
41-
pml.payment_account_id = outstanding_account
42-
4331
cls.sale_journal = cls.env["account.journal"].search(
4432
[("company_id", "=", cls.company.id), ("type", "=", "sale")], limit=1
4533
)
4634
cls.purchase_journal = cls.env["account.journal"].search(
4735
[("company_id", "=", cls.company.id), ("type", "=", "purchase")], limit=1
4836
)
37+
assert cls.pay_journal, "Se necesita un journal type=bank en la compañía"
38+
assert cls.sale_journal, "Se necesita un journal type=sale en la compañía"
39+
assert cls.purchase_journal, "Se necesita un journal type=purchase en la compañía"
40+
41+
# Para métodos manuales, usar la cuenta default del diario (no outstanding).
42+
# Con outstanding, Odoo deja el pago como "in_payment" hasta que se concilie
43+
# con un extracto bancario — lo que invalidaría los asserts de payment_state.
44+
for pml in cls.pay_journal.inbound_payment_method_line_ids:
45+
if pml.payment_method_id.code == "manual":
46+
pml.payment_account_id = cls.pay_journal.default_account_id
47+
for pml in cls.pay_journal.outbound_payment_method_line_ids:
48+
if pml.payment_method_id.code == "manual":
49+
pml.payment_account_id = cls.pay_journal.default_account_id
4950

5051
ar = cls.env.ref("base.ar", raise_if_not_found=False)
5152
partner_vals = {"name": "Pay Now Partner"}

account_payment_pro/views/account_payment_view.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
<field name="accounting_rate_inverted" invisible="True"/>
104104
<!-- Campo técnico: round-trip de la moneda anterior para _onchange_currency_recompute_amount -->
105105
<field name="previous_currency_id" invisible="True"/>
106+
<field name="amount_exact" invisible="True"/>
106107
<!-- Importe en moneda de contrapartida: visible si A != B -->
107108
<!-- TODO ver si encontramos una mejor opcion y/o podemos ponerle la negrita tmb al destination_currency_id -->
108109
<div class="o_row" invisible="currency_id == counterpart_currency_id" name="counterpart_currency_amount">

0 commit comments

Comments
 (0)