221 lines
8.6 KiB
Python
221 lines
8.6 KiB
Python
import re
|
||
from math import copysign
|
||
|
||
from odoo import _, models, Command
|
||
from odoo.exceptions import UserError
|
||
|
||
|
||
class ReconcileModelLine(models.Model):
|
||
"""Extends reconciliation model lines with methods for computing
|
||
journal item values across manual and bank reconciliation contexts."""
|
||
|
||
_inherit = 'account.reconcile.model.line'
|
||
|
||
# ------------------------------------------------------------------
|
||
# Core helpers
|
||
# ------------------------------------------------------------------
|
||
|
||
def _resolve_taxes_for_partner(self, partner):
|
||
"""Return the tax recordset that should be applied, taking fiscal
|
||
position mapping into account when a partner is provided."""
|
||
tax_records = self.tax_ids
|
||
if not tax_records or not partner:
|
||
return tax_records
|
||
fpos = self.env['account.fiscal.position']._get_fiscal_position(partner)
|
||
if fpos:
|
||
tax_records = fpos.map_tax(tax_records)
|
||
return tax_records
|
||
|
||
def _prepare_aml_vals(self, partner):
|
||
"""Build a base dictionary of account.move.line values derived from
|
||
this reconciliation model line.
|
||
|
||
Fiscal-position tax remapping is applied automatically when the
|
||
supplied *partner* record has a matching fiscal position.
|
||
|
||
Args:
|
||
partner: ``res.partner`` record to attach to the move line.
|
||
|
||
Returns:
|
||
``dict`` suitable for later account.move.line creation.
|
||
"""
|
||
self.ensure_one()
|
||
|
||
mapped_taxes = self._resolve_taxes_for_partner(partner)
|
||
|
||
result_values = {
|
||
'name': self.label,
|
||
'partner_id': partner.id,
|
||
'analytic_distribution': self.analytic_distribution,
|
||
'tax_ids': [Command.set(mapped_taxes.ids)],
|
||
'reconcile_model_id': self.model_id.id,
|
||
}
|
||
|
||
if self.account_id:
|
||
result_values['account_id'] = self.account_id.id
|
||
|
||
return result_values
|
||
|
||
# ------------------------------------------------------------------
|
||
# Manual reconciliation
|
||
# ------------------------------------------------------------------
|
||
|
||
def _compute_manual_amount(self, remaining_balance, currency):
|
||
"""Derive the line amount for manual reconciliation based on the
|
||
configured amount type (percentage or fixed).
|
||
|
||
Raises ``UserError`` for amount types that are only valid inside the
|
||
bank reconciliation widget (e.g. regex, percentage_st_line).
|
||
"""
|
||
if self.amount_type == 'percentage':
|
||
return currency.round(remaining_balance * self.amount / 100.0)
|
||
|
||
if self.amount_type == 'fixed':
|
||
direction = 1 if remaining_balance > 0.0 else -1
|
||
return currency.round(self.amount * direction)
|
||
|
||
raise UserError(
|
||
_("This reconciliation model cannot be applied in the manual "
|
||
"reconciliation widget because its amount type is not supported "
|
||
"in that context.")
|
||
)
|
||
|
||
def _apply_in_manual_widget(self, residual_amount_currency, partner, currency):
|
||
"""Produce move-line values for the manual reconciliation widget.
|
||
|
||
The ``journal_id`` field is deliberately included in the result even
|
||
though it is a related (read-only) field on the move line. The manual
|
||
reconciliation widget relies on its presence to group lines into a
|
||
single journal entry per journal.
|
||
|
||
Args:
|
||
residual_amount_currency: Open balance in the account's currency.
|
||
partner: ``res.partner`` record for the counterpart.
|
||
currency: ``res.currency`` record used by the account.
|
||
|
||
Returns:
|
||
``dict`` ready for account.move.line creation.
|
||
"""
|
||
self.ensure_one()
|
||
|
||
computed_amount = self._compute_manual_amount(residual_amount_currency, currency)
|
||
|
||
line_data = self._prepare_aml_vals(partner)
|
||
line_data.update({
|
||
'currency_id': currency.id,
|
||
'amount_currency': computed_amount,
|
||
'journal_id': self.journal_id.id,
|
||
})
|
||
return line_data
|
||
|
||
# ------------------------------------------------------------------
|
||
# Bank reconciliation
|
||
# ------------------------------------------------------------------
|
||
|
||
def _extract_regex_amount(self, payment_ref, residual_balance):
|
||
"""Try to extract a numeric amount from *payment_ref* using the
|
||
regex pattern stored on this line.
|
||
|
||
Returns the parsed amount with the correct sign, or ``0.0`` when
|
||
parsing fails or the pattern does not match.
|
||
"""
|
||
pattern_match = re.search(self.amount_string, payment_ref)
|
||
if not pattern_match:
|
||
return 0.0
|
||
|
||
separator = self.model_id.decimal_separator
|
||
direction = 1 if residual_balance > 0.0 else -1
|
||
try:
|
||
raw_group = pattern_match.group(1)
|
||
digits_only = re.sub(r'[^\d' + separator + ']', '', raw_group)
|
||
parsed_value = float(digits_only.replace(separator, '.'))
|
||
return copysign(parsed_value * direction, residual_balance)
|
||
except (ValueError, IndexError):
|
||
return 0.0
|
||
|
||
def _compute_percentage_st_line_amount(self, st_line, currency):
|
||
"""Calculate the move-line amount and currency when the amount type
|
||
is ``percentage_st_line``.
|
||
|
||
Depending on the model configuration the calculation uses either the
|
||
raw transaction figures or the journal-currency figures.
|
||
|
||
Returns a ``(computed_amount, target_currency)`` tuple.
|
||
"""
|
||
(
|
||
txn_amount, txn_currency,
|
||
jnl_amount, jnl_currency,
|
||
_comp_amount, _comp_currency,
|
||
) = st_line._get_accounting_amounts_and_currencies()
|
||
|
||
ratio = self.amount / 100.0
|
||
is_invoice_writeoff = (
|
||
self.model_id.rule_type == 'writeoff_button'
|
||
and self.model_id.counterpart_type in ('sale', 'purchase')
|
||
)
|
||
|
||
if is_invoice_writeoff:
|
||
# Invoice creation – use the original transaction currency.
|
||
return currency.round(-txn_amount * ratio), txn_currency, ratio
|
||
# Standard write-off – follow the journal currency.
|
||
return currency.round(-jnl_amount * ratio), jnl_currency, None
|
||
|
||
def _apply_in_bank_widget(self, residual_amount_currency, partner, st_line):
|
||
"""Produce move-line values for the bank reconciliation widget.
|
||
|
||
Handles three amount-type strategies:
|
||
|
||
* ``percentage_st_line`` – percentage of the statement line amount
|
||
* ``regex`` – amount extracted from the payment reference
|
||
* fallback – delegates to :meth:`_apply_in_manual_widget`
|
||
|
||
Args:
|
||
residual_amount_currency: Open balance in the statement line currency.
|
||
partner: ``res.partner`` record for the counterpart.
|
||
st_line: ``account.bank.statement.line`` being reconciled.
|
||
|
||
Returns:
|
||
``dict`` ready for account.move.line creation.
|
||
"""
|
||
self.ensure_one()
|
||
|
||
line_currency = (
|
||
st_line.foreign_currency_id
|
||
or st_line.journal_id.currency_id
|
||
or st_line.company_currency_id
|
||
)
|
||
|
||
# -- percentage of statement line ---------------------------------
|
||
if self.amount_type == 'percentage_st_line':
|
||
computed_amount, target_cur, pct_ratio = (
|
||
self._compute_percentage_st_line_amount(st_line, line_currency)
|
||
)
|
||
entry_data = self._prepare_aml_vals(partner)
|
||
entry_data['currency_id'] = target_cur.id
|
||
entry_data['amount_currency'] = computed_amount
|
||
if pct_ratio is not None:
|
||
entry_data['percentage_st_line'] = pct_ratio
|
||
if not entry_data.get('name'):
|
||
entry_data['name'] = st_line.payment_ref
|
||
return entry_data
|
||
|
||
# -- regex extraction from payment reference ----------------------
|
||
if self.amount_type == 'regex':
|
||
extracted = self._extract_regex_amount(
|
||
st_line.payment_ref, residual_amount_currency,
|
||
)
|
||
entry_data = self._prepare_aml_vals(partner)
|
||
entry_data['currency_id'] = line_currency.id
|
||
entry_data['amount_currency'] = extracted
|
||
if not entry_data.get('name'):
|
||
entry_data['name'] = st_line.payment_ref
|
||
return entry_data
|
||
|
||
# -- percentage / fixed – reuse manual widget logic ---------------
|
||
entry_data = self._apply_in_manual_widget(
|
||
residual_amount_currency, partner, line_currency,
|
||
)
|
||
if not entry_data.get('name'):
|
||
entry_data['name'] = st_line.payment_ref
|
||
return entry_data
|