Skip to content

[16.0][FIX] pms_autoinvoice, pms_downpayment_lifecycle: down payments in automatic invoices - #170

Merged
DarioLodeiros merged 3 commits into
16.0from
16.0-fix-autoinvoice-downpayment-deduction
Oct 13, 2026
Merged

DarioLodeiros merged 3 commits into
16.0from
16.0-fix-autoinvoice-downpayment-deduction

Conversation

@DarioLodeiros

@DarioLodeiros DarioLodeiros commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

pms_autoinvoice

The automatic invoice deducts the folio's down payments from the invoice of their customer. That step had several defects:

  • folio.sale.line._prepare_invoice_line() maps the taxes through the invoice fiscal position and has no default for it. The call did not pass it, so every automatic invoice with a down payment of its own customer failed with AttributeError: 'NoneType' object has no attribute 'map_tax'. The cron does not retry a folio once its day has passed, so those folios were left uninvoiced.
  • A down payment was added to every invoice of its customer. A folio split in two invoices for the same customer (for instance, lines with no "invoice to") got it deducted twice, or failed with "Expected singleton".
  • It was added even when it left the invoice negative, for example when the stay had already been invoiced by hand and only some extras were left. With delay_post the negative draft was committed, and its posting job then failed, leaving a draft that cannot be posted.
  • When a simplified invoice over the limit was moved to a named host, the down payment was still deducted from what was left for the original customer. The host's invoice, which bills the stay, settles it as well, so it ended up handled twice.

Now each down payment is deducted at most once per run, from the largest invoice of its customer that it fits in without going negative. It is left to the final invoice when its customer was moved to a host, and when it fits nowhere. In that last case the folio gets a message.

pms_downpayment_lifecycle

pms_autoinvoice reverses the down payments that the final invoice did not deduct with cancel=True, which unreconciles them from their payment. For a down payment of a locked period, that rewrites the customer balances of a month that is already closed.

The _reverse_downpayment_invoices hook, added for exactly this case, now leaves those down payments to the transfer. The transfer settles them when the final invoice is posted, with a credit note issued with cancel=False and a transfer entry, so the closed period keeps its reconciliations. Down payments of an open period are reversed as before.

The module's credit notes also came out without an SII refund type. _rectify_downpayment reversed the down payment with no sii_refund_type in the context. The field is a stored computed field that can be edited, so the reversal copies the down payment's empty value and the compute never runs, and the AEAT rejects the credit note (TipoRectificativa). It now passes sii_refund_type='I' through the context, as the other callers of _reverse_moves already do. This covers every credit note of the module, because they all go through this method.

Tests

  • pms_autoinvoice:
    • deduction through the automatic invoice (the map_tax case);
    • one deduction when there are two invoices of the same customer;
    • a down payment that does not fit is not deducted, and the helper returns it;
    • no deduction when the customer was moved to a host.
  • pms_downpayment_lifecycle:
    • a locked-period down payment is not reversed by the automatic invoice, and an open-period one still is;
    • the rectification passes the SII refund type.

These are accounting tests, so CI skips them when no chart of accounts is available. They were run locally on a fresh database with l10n_generic_coa: 20 tests in pms_autoinvoice and 43 in pms_downpayment_lifecycle, all passing. As a negative control, the new tests were also run against the code without this change: they fail with the original map_tax error, the locked-period down payment gets reversed, and the credit note gets no SII refund type.

…it fits

The automatic invoice deducts the folio's down payments from the invoice
of their customer, and that step had several defects:

- folio.sale.line._prepare_invoice_line() maps the taxes through the
  invoice fiscal position and has no default for it. The call did not
  pass it, so any automatic invoice with a down payment of its own
  customer failed with "'NoneType' object has no attribute 'map_tax'".
- A down payment was added to every invoice of its customer. A folio
  split in two invoices for the same customer got it deducted twice, or
  failed with "Expected singleton".
- It was added even when it left the invoice negative. With delay_post
  the negative draft was committed and its posting job then failed,
  leaving a draft that cannot be posted.
- When a simplified invoice over the limit is moved to a named host, the
  down payment was still deducted from what was left for the original
  customer, while the host's invoice, the one billing the stay, settles
  it as well.

Each down payment is now deducted at most once per run, from the largest
invoice of its customer that it fits in without going negative. It is
left to the final invoice when its customer was moved to a host, and when
it fits nowhere, in which case the folio says so.
… the transfer

pms_autoinvoice reverses the down payments that the final invoice did
not deduct with cancel=True, which unreconciles them from their payment.
For a down payment of a locked period that rewrites the customer
balances of a month that is already closed.

The _reverse_downpayment_invoices hook now leaves those down payments to
the transfer, which settles them when the final invoice is posted:
credit note with cancel=False and a transfer entry, so the closed period
keeps its reconciliations. Down payments of an open period are reversed
as before.
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Diff Coverage

Diff: origin/16.0...HEAD, staged and unstaged changes

  • pms_autoinvoice/models/pms_property.py (33.3%): Missing lines 224-231,237-239,241,244,248,318,336
  • pms_downpayment_lifecycle/models/account_move.py (55.6%): Missing lines 402,582-583,587

Summary

  • Total: 33 lines
  • Missing: 20 lines
  • Coverage: 39%

pms_autoinvoice/models/pms_property.py

Lines 220-235

  220         to look at.
  221         """
  222         not_fitting = self.env["folio.sale.line"]
  223         for downpayment in downpayments:
! 224             partner = downpayment.default_invoice_to
! 225             if partner in switched_partners:
! 226                 continue
! 227             candidates = invoices.filtered(lambda inv, p=partner: inv.partner_id == p)
! 228             if not candidates:
! 229                 continue
! 230             for invoice in candidates.sorted("amount_total", reverse=True):
! 231                 vals = downpayment._prepare_invoice_line(
  232                     sequence=max(invoice.invoice_line_ids.mapped("sequence")) + 1,
  233                     # pms maps the taxes through the invoice fiscal position
  234                     # and has no default for it.
  235                     invoice_fpos=invoice.fiscal_position_id,

Lines 233-252

  233                     # pms maps the taxes through the invoice fiscal position
  234                     # and has no default for it.
  235                     invoice_fpos=invoice.fiscal_position_id,
  236                 )
! 237                 invoice.write({"invoice_line_ids": [(0, 0, vals)]})
! 238                 if invoice.currency_id.compare_amounts(invoice.amount_total, 0.0) >= 0:
! 239                     break
  240                 # It would leave the invoice negative: take it back out.
! 241                 deduction = invoice.invoice_line_ids.filtered(
  242                     lambda line, d=downpayment: d in line.folio_line_ids
  243                 )
! 244                 invoice.write(
  245                     {"invoice_line_ids": [(2, line.id) for line in deduction]}
  246                 )
  247             else:
! 248                 not_fitting |= downpayment
  249         return not_fitting
  250 
  251     def autoinvoice_folio(self, folio, delay_post=False):
  252         try:

Lines 314-322

  314                             hosts_to_invoice = all_invoice_partners.filtered(
  315                                 lambda p: p._check_enought_invoice_data()
  316                             ).mapped("id")
  317                         if hosts_to_invoice:
! 318                             switched_partners |= invoice.partner_id
  319                             invoice.partner_id = hosts_to_invoice[0]
  320                             invoice.journal_id = (
  321                                 invoice.pms_property_id.journal_normal_invoice_id
  322                             )

Lines 332-340

  332                 not_fitting = self._autoinvoice_deduct_downpayments(
  333                     invoices, downpayments, switched_partners
  334                 )
  335                 if not_fitting:
! 336                     folio.sudo().message_post(
  337                         body=_(
  338                             "Down payments not deducted in the automatic invoice, "
  339                             "because they are larger than what it bills: %s. "
  340                             "They are left for review."

pms_downpayment_lifecycle/models/account_move.py

Lines 398-406

  398         context the AEAT rejects the credit note (TipoRectificativa). The
  399         context key means nothing when the SII module is not installed.
  400         """
  401         self.ensure_one()
! 402         credit_note = self.with_context(sii_refund_type="I")._reverse_moves(
  403             default_values_list=[
  404                 {
  405                     "date": date,
  406                     "invoice_date": date,

Lines 578-588

  578     def _is_in_locked_period(self):
  579         """Read from the company, not from ``_get_user_fiscal_lock_date``: the
  580         cron runs as a user that only sees the fiscal year lock, and the
  581         closed month is locked through ``period_lock_date``."""
! 582         self.ensure_one()
! 583         lock_date = max(
  584             self.company_id.period_lock_date or date.min,
  585             self.company_id.fiscalyear_lock_date or date.min,
  586         )
! 587         return self.date <= lock_date

…it notes

_rectify_downpayment reversed the down payment without sii_refund_type in
the context. The field is a stored computed field that can be edited, so
the reversal copies the down payment's empty value onto the credit note
and the compute never runs: the AEAT rejects those credit notes with
"Valor o tipo incorrecto del campo: TipoRectificativa".

Pass sii_refund_type='I' through the context, as the other callers of
_reverse_moves do. Every credit note of the module (transfer, apply,
refund and cancellation) goes through this method.
@DarioLodeiros
DarioLodeiros merged commit 516f0f6 into 16.0 Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant