Initial commit
This commit is contained in:
697
Fusion Accounting/models/bank_rec_widget_line.py
Normal file
697
Fusion Accounting/models/bank_rec_widget_line.py
Normal file
@@ -0,0 +1,697 @@
|
||||
# Fusion Accounting - Bank Reconciliation Widget Line
|
||||
# Original implementation for Fusion Accounting module
|
||||
|
||||
import uuid
|
||||
import markupsafe
|
||||
|
||||
from odoo import _, api, fields, models, Command
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.misc import formatLang, frozendict
|
||||
|
||||
|
||||
# Flags that derive their values directly from the source journal entry
|
||||
_SOURCE_LINKED_FLAGS = frozenset({'aml', 'new_aml', 'liquidity', 'exchange_diff'})
|
||||
# Flags where the statement line date should be used
|
||||
_STMT_DATE_FLAGS = frozenset({'liquidity', 'auto_balance', 'manual', 'early_payment', 'tax_line'})
|
||||
# Flags that derive partner from the source journal entry
|
||||
_PARTNER_FROM_SOURCE_FLAGS = frozenset({'aml', 'new_aml'})
|
||||
# Flags that use the widget's partner
|
||||
_PARTNER_FROM_WIDGET_FLAGS = frozenset({'liquidity', 'auto_balance', 'manual', 'early_payment', 'tax_line'})
|
||||
# Flags that derive currency from the transaction
|
||||
_TRANSACTION_CURRENCY_FLAGS = frozenset({'auto_balance', 'manual', 'early_payment'})
|
||||
|
||||
|
||||
class FusionBankRecLine(models.Model):
|
||||
"""Represents a single entry within the bank reconciliation widget.
|
||||
|
||||
Each entry has a 'flag' indicating its role in the reconciliation process:
|
||||
- liquidity: The bank/cash journal item from the statement line
|
||||
- new_aml: A journal item being matched against the statement line
|
||||
- aml: An already-reconciled journal item (read-only display)
|
||||
- exchange_diff: Automatically generated foreign exchange adjustment
|
||||
- tax_line: Tax amount computed from manual entries
|
||||
- manual: A user-created write-off or adjustment entry
|
||||
- early_payment: Discount entry for early payment terms
|
||||
- auto_balance: System-generated balancing entry
|
||||
|
||||
This model exists only in memory; no database table is created.
|
||||
"""
|
||||
|
||||
_name = "bank.rec.widget.line"
|
||||
_inherit = "analytic.mixin"
|
||||
_description = "Fusion bank reconciliation entry"
|
||||
|
||||
_auto = False
|
||||
_table_query = "0"
|
||||
|
||||
# --- Relationship to parent widget ---
|
||||
wizard_id = fields.Many2one(comodel_name='bank.rec.widget')
|
||||
index = fields.Char(compute='_compute_index')
|
||||
flag = fields.Selection(
|
||||
selection=[
|
||||
('liquidity', 'liquidity'),
|
||||
('new_aml', 'new_aml'),
|
||||
('aml', 'aml'),
|
||||
('exchange_diff', 'exchange_diff'),
|
||||
('tax_line', 'tax_line'),
|
||||
('manual', 'manual'),
|
||||
('early_payment', 'early_payment'),
|
||||
('auto_balance', 'auto_balance'),
|
||||
],
|
||||
)
|
||||
|
||||
# --- Core accounting fields ---
|
||||
journal_default_account_id = fields.Many2one(
|
||||
related='wizard_id.st_line_id.journal_id.default_account_id',
|
||||
depends=['wizard_id'],
|
||||
)
|
||||
account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
compute='_compute_account_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
check_company=True,
|
||||
domain="""[
|
||||
('id', '!=', journal_default_account_id),
|
||||
('account_type', 'not in', ('asset_cash', 'off_balance')),
|
||||
]""",
|
||||
)
|
||||
date = fields.Date(
|
||||
compute='_compute_date',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
name = fields.Char(
|
||||
compute='_compute_name',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
compute='_compute_partner_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
compute='_compute_currency_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
company_id = fields.Many2one(related='wizard_id.company_id')
|
||||
country_code = fields.Char(related='company_id.country_id.code', depends=['company_id'])
|
||||
company_currency_id = fields.Many2one(related='wizard_id.company_currency_id')
|
||||
|
||||
amount_currency = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
compute='_compute_amount_currency',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
balance = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_balance',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
# --- Transaction currency fields (from statement line) ---
|
||||
transaction_currency_id = fields.Many2one(
|
||||
related='wizard_id.st_line_id.foreign_currency_id',
|
||||
depends=['wizard_id'],
|
||||
)
|
||||
amount_transaction_currency = fields.Monetary(
|
||||
currency_field='transaction_currency_id',
|
||||
related='wizard_id.st_line_id.amount_currency',
|
||||
depends=['wizard_id'],
|
||||
)
|
||||
|
||||
# --- Debit/Credit split ---
|
||||
debit = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_from_balance',
|
||||
)
|
||||
credit = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_from_balance',
|
||||
)
|
||||
|
||||
# --- Tax handling ---
|
||||
force_price_included_taxes = fields.Boolean()
|
||||
tax_base_amount_currency = fields.Monetary(currency_field='currency_id')
|
||||
|
||||
# --- Source journal entry reference ---
|
||||
source_aml_id = fields.Many2one(comodel_name='account.move.line')
|
||||
source_aml_move_id = fields.Many2one(
|
||||
comodel_name='account.move',
|
||||
compute='_compute_source_aml_fields',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
source_aml_move_name = fields.Char(
|
||||
compute='_compute_source_aml_fields',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
# --- Tax detail fields ---
|
||||
tax_repartition_line_id = fields.Many2one(
|
||||
comodel_name='account.tax.repartition.line',
|
||||
compute='_compute_tax_repartition_line_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
tax_ids = fields.Many2many(
|
||||
comodel_name='account.tax',
|
||||
compute='_compute_tax_ids',
|
||||
store=True,
|
||||
readonly=False,
|
||||
check_company=True,
|
||||
)
|
||||
tax_tag_ids = fields.Many2many(
|
||||
comodel_name='account.account.tag',
|
||||
compute='_compute_tax_tag_ids',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
group_tax_id = fields.Many2one(
|
||||
comodel_name='account.tax',
|
||||
compute='_compute_group_tax_id',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
# --- Reconcile model tracking ---
|
||||
reconcile_model_id = fields.Many2one(comodel_name='account.reconcile.model')
|
||||
|
||||
# --- Original (pre-partial) amounts for comparison ---
|
||||
source_amount_currency = fields.Monetary(currency_field='currency_id')
|
||||
source_balance = fields.Monetary(currency_field='company_currency_id')
|
||||
source_debit = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_from_source_balance',
|
||||
)
|
||||
source_credit = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_from_source_balance',
|
||||
)
|
||||
|
||||
# --- Visual indicators for partial amounts ---
|
||||
display_stroked_amount_currency = fields.Boolean(compute='_compute_display_stroked_amount_currency')
|
||||
display_stroked_balance = fields.Boolean(compute='_compute_display_stroked_balance')
|
||||
|
||||
# --- Partner account info for UI suggestions ---
|
||||
partner_currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
compute='_compute_partner_info',
|
||||
)
|
||||
partner_receivable_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
compute='_compute_partner_info',
|
||||
)
|
||||
partner_payable_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
compute='_compute_partner_info',
|
||||
)
|
||||
partner_receivable_amount = fields.Monetary(
|
||||
currency_field='partner_currency_id',
|
||||
compute='_compute_partner_info',
|
||||
)
|
||||
partner_payable_amount = fields.Monetary(
|
||||
currency_field='partner_currency_id',
|
||||
compute='_compute_partner_info',
|
||||
)
|
||||
|
||||
# --- Display fields ---
|
||||
bank_account = fields.Char(compute='_compute_bank_account')
|
||||
suggestion_html = fields.Html(
|
||||
compute='_compute_suggestion',
|
||||
sanitize=False,
|
||||
)
|
||||
suggestion_amount_currency = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
compute='_compute_suggestion',
|
||||
)
|
||||
suggestion_balance = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_suggestion',
|
||||
)
|
||||
ref = fields.Char(
|
||||
compute='_compute_ref_narration',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
narration = fields.Html(
|
||||
compute='_compute_ref_narration',
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
manually_modified = fields.Boolean()
|
||||
|
||||
# =========================================================================
|
||||
# COMPUTE METHODS
|
||||
# =========================================================================
|
||||
|
||||
def _compute_index(self):
|
||||
"""Assign a unique identifier to each entry for JS-side tracking."""
|
||||
for entry in self:
|
||||
entry.index = uuid.uuid4()
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_account_id(self):
|
||||
"""Derive the account from the source journal item for linked entries.
|
||||
|
||||
Entries tied to actual journal items (aml, new_aml, liquidity, exchange_diff)
|
||||
inherit the account directly. Other entry types retain their current account.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in _SOURCE_LINKED_FLAGS:
|
||||
entry.account_id = entry.source_aml_id.account_id
|
||||
else:
|
||||
entry.account_id = entry.account_id
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_date(self):
|
||||
"""Set the date based on the entry type.
|
||||
|
||||
Source-linked entries (aml, new_aml, exchange_diff) use the original journal
|
||||
item date. Statement-based entries use the statement line date.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in _STMT_DATE_FLAGS:
|
||||
entry.date = entry.wizard_id.st_line_id.date
|
||||
elif entry.flag in ('aml', 'new_aml', 'exchange_diff'):
|
||||
entry.date = entry.source_aml_id.date
|
||||
else:
|
||||
entry.date = entry.date
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_name(self):
|
||||
"""Set the description/label from the source journal item when applicable.
|
||||
|
||||
For entries derived from journal items, the label is taken from the
|
||||
original item. If the source has no name (e.g. credit notes), the
|
||||
move name is used as fallback.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in ('aml', 'new_aml', 'liquidity'):
|
||||
entry.name = entry.source_aml_id.name or entry.source_aml_move_name
|
||||
else:
|
||||
entry.name = entry.name
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_partner_id(self):
|
||||
"""Determine the partner for each entry based on its type.
|
||||
|
||||
Matched journal items carry their own partner. Statement-derived
|
||||
entries use the partner set on the reconciliation widget.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in _PARTNER_FROM_SOURCE_FLAGS:
|
||||
entry.partner_id = entry.source_aml_id.partner_id
|
||||
elif entry.flag in _PARTNER_FROM_WIDGET_FLAGS:
|
||||
entry.partner_id = entry.wizard_id.partner_id
|
||||
else:
|
||||
entry.partner_id = entry.partner_id
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_currency_id(self):
|
||||
"""Set the currency based on entry type.
|
||||
|
||||
Source-linked entries use the currency from the original journal item.
|
||||
Transaction-related entries (auto_balance, manual, early_payment) use
|
||||
the transaction currency from the bank statement.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in _SOURCE_LINKED_FLAGS:
|
||||
entry.currency_id = entry.source_aml_id.currency_id
|
||||
elif entry.flag in _TRANSACTION_CURRENCY_FLAGS:
|
||||
entry.currency_id = entry.wizard_id.transaction_currency_id
|
||||
else:
|
||||
entry.currency_id = entry.currency_id
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_balance(self):
|
||||
"""Set the company-currency balance from the source when applicable.
|
||||
|
||||
Only 'aml' and 'liquidity' entries copy the balance directly from the
|
||||
source journal item. All other types preserve their computed/manual balance.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in ('aml', 'liquidity'):
|
||||
entry.balance = entry.source_aml_id.balance
|
||||
else:
|
||||
entry.balance = entry.balance
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_amount_currency(self):
|
||||
"""Set the foreign currency amount from the source when applicable.
|
||||
|
||||
Only 'aml' and 'liquidity' entries copy directly from the source.
|
||||
"""
|
||||
for entry in self:
|
||||
if entry.flag in ('aml', 'liquidity'):
|
||||
entry.amount_currency = entry.source_aml_id.amount_currency
|
||||
else:
|
||||
entry.amount_currency = entry.amount_currency
|
||||
|
||||
@api.depends('balance')
|
||||
def _compute_from_balance(self):
|
||||
"""Split the balance into separate debit and credit components."""
|
||||
for entry in self:
|
||||
entry.debit = max(entry.balance, 0.0)
|
||||
entry.credit = max(-entry.balance, 0.0)
|
||||
|
||||
@api.depends('source_balance')
|
||||
def _compute_from_source_balance(self):
|
||||
"""Split the original source balance into debit and credit."""
|
||||
for entry in self:
|
||||
entry.source_debit = max(entry.source_balance, 0.0)
|
||||
entry.source_credit = max(-entry.source_balance, 0.0)
|
||||
|
||||
@api.depends('source_aml_id', 'account_id', 'partner_id')
|
||||
def _compute_analytic_distribution(self):
|
||||
"""Compute analytic distribution based on entry type.
|
||||
|
||||
Source-linked entries (liquidity, aml) inherit from the source item.
|
||||
Tax/early-payment entries keep their current distribution. Other entries
|
||||
look up the default distribution from analytic distribution models.
|
||||
"""
|
||||
distribution_cache = {}
|
||||
for entry in self:
|
||||
if entry.flag in ('liquidity', 'aml'):
|
||||
entry.analytic_distribution = entry.source_aml_id.analytic_distribution
|
||||
elif entry.flag in ('tax_line', 'early_payment'):
|
||||
entry.analytic_distribution = entry.analytic_distribution
|
||||
else:
|
||||
lookup_params = frozendict({
|
||||
"partner_id": entry.partner_id.id,
|
||||
"partner_category_id": entry.partner_id.category_id.ids,
|
||||
"account_prefix": entry.account_id.code,
|
||||
"company_id": entry.company_id.id,
|
||||
})
|
||||
if lookup_params not in distribution_cache:
|
||||
distribution_cache[lookup_params] = (
|
||||
self.env['account.analytic.distribution.model']._get_distribution(lookup_params)
|
||||
)
|
||||
entry.analytic_distribution = distribution_cache[lookup_params] or entry.analytic_distribution
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_tax_repartition_line_id(self):
|
||||
"""Inherit tax repartition line from the source for 'aml' entries only."""
|
||||
for entry in self:
|
||||
if entry.flag == 'aml':
|
||||
entry.tax_repartition_line_id = entry.source_aml_id.tax_repartition_line_id
|
||||
else:
|
||||
entry.tax_repartition_line_id = entry.tax_repartition_line_id
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_tax_ids(self):
|
||||
"""Copy applied tax references from the source for 'aml' entries."""
|
||||
for entry in self:
|
||||
if entry.flag == 'aml':
|
||||
entry.tax_ids = [Command.set(entry.source_aml_id.tax_ids.ids)]
|
||||
else:
|
||||
entry.tax_ids = entry.tax_ids
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_tax_tag_ids(self):
|
||||
"""Copy tax tags from the source for 'aml' entries."""
|
||||
for entry in self:
|
||||
if entry.flag == 'aml':
|
||||
entry.tax_tag_ids = [Command.set(entry.source_aml_id.tax_tag_ids.ids)]
|
||||
else:
|
||||
entry.tax_tag_ids = entry.tax_tag_ids
|
||||
|
||||
@api.depends('source_aml_id')
|
||||
def _compute_group_tax_id(self):
|
||||
"""Copy the group tax reference from the source for 'aml' entries."""
|
||||
for entry in self:
|
||||
if entry.flag == 'aml':
|
||||
entry.group_tax_id = entry.source_aml_id.group_tax_id
|
||||
else:
|
||||
entry.group_tax_id = entry.group_tax_id
|
||||
|
||||
@api.depends('currency_id', 'amount_currency', 'source_amount_currency')
|
||||
def _compute_display_stroked_amount_currency(self):
|
||||
"""Determine whether to show a strikethrough on the foreign currency amount.
|
||||
|
||||
This visual indicator appears when a 'new_aml' entry has been partially
|
||||
matched (its current amount differs from the original source amount).
|
||||
"""
|
||||
for entry in self:
|
||||
is_modified = entry.currency_id.compare_amounts(
|
||||
entry.amount_currency, entry.source_amount_currency
|
||||
) != 0
|
||||
entry.display_stroked_amount_currency = entry.flag == 'new_aml' and is_modified
|
||||
|
||||
@api.depends('currency_id', 'balance', 'source_balance')
|
||||
def _compute_display_stroked_balance(self):
|
||||
"""Determine whether to show a strikethrough on the balance.
|
||||
|
||||
Applies to 'new_aml' and 'exchange_diff' entries whose balance
|
||||
has been adjusted from the original source value.
|
||||
"""
|
||||
for entry in self:
|
||||
balance_changed = entry.currency_id.compare_amounts(
|
||||
entry.balance, entry.source_balance
|
||||
) != 0
|
||||
entry.display_stroked_balance = (
|
||||
entry.flag in ('new_aml', 'exchange_diff') and balance_changed
|
||||
)
|
||||
|
||||
@api.depends('flag')
|
||||
def _compute_source_aml_fields(self):
|
||||
"""Resolve the originating move for display and navigation purposes.
|
||||
|
||||
For 'new_aml' and 'liquidity' entries, this is simply the move containing
|
||||
the source journal item. For 'aml' (already reconciled) entries, we trace
|
||||
through partial reconciliation records to find the counterpart document.
|
||||
"""
|
||||
for entry in self:
|
||||
entry.source_aml_move_id = None
|
||||
entry.source_aml_move_name = None
|
||||
|
||||
if entry.flag in ('new_aml', 'liquidity'):
|
||||
originating_move = entry.source_aml_id.move_id
|
||||
entry.source_aml_move_id = originating_move
|
||||
entry.source_aml_move_name = originating_move.name
|
||||
elif entry.flag == 'aml':
|
||||
# Trace through reconciliation partials to find the counterpart
|
||||
partial_records = (
|
||||
entry.source_aml_id.matched_debit_ids
|
||||
+ entry.source_aml_id.matched_credit_ids
|
||||
)
|
||||
linked_items = partial_records.debit_move_id + partial_records.credit_move_id
|
||||
# Exclude the source itself and any exchange difference entries
|
||||
fx_move_items = partial_records.exchange_move_id.line_ids
|
||||
counterpart_items = linked_items - entry.source_aml_id - fx_move_items
|
||||
if len(counterpart_items) == 1:
|
||||
entry.source_aml_move_id = counterpart_items.move_id
|
||||
entry.source_aml_move_name = counterpart_items.move_id.name
|
||||
|
||||
@api.depends('wizard_id.form_index', 'partner_id')
|
||||
def _compute_partner_info(self):
|
||||
"""Load receivable/payable account info for the selected partner.
|
||||
|
||||
This data is used by the UI to offer account switching suggestions
|
||||
when a partner is set on a manual entry. Only computed for the
|
||||
entry currently being edited (matching the form_index).
|
||||
"""
|
||||
for entry in self:
|
||||
# Set defaults
|
||||
entry.partner_receivable_amount = 0.0
|
||||
entry.partner_payable_amount = 0.0
|
||||
entry.partner_currency_id = None
|
||||
entry.partner_receivable_account_id = None
|
||||
entry.partner_payable_account_id = None
|
||||
|
||||
# Only compute for the actively edited entry with a partner
|
||||
if not entry.partner_id or entry.index != entry.wizard_id.form_index:
|
||||
continue
|
||||
|
||||
entry.partner_currency_id = entry.company_currency_id
|
||||
scoped_partner = entry.partner_id.with_company(entry.wizard_id.company_id)
|
||||
posted_filter = [('parent_state', '=', 'posted'), ('partner_id', '=', scoped_partner.id)]
|
||||
|
||||
# Receivable info
|
||||
recv_account = scoped_partner.property_account_receivable_id
|
||||
entry.partner_receivable_account_id = recv_account
|
||||
if recv_account:
|
||||
recv_domain = expression.AND([posted_filter, [('account_id', '=', recv_account.id)]])
|
||||
recv_data = self.env['account.move.line']._read_group(
|
||||
domain=recv_domain,
|
||||
aggregates=['amount_residual:sum'],
|
||||
)
|
||||
entry.partner_receivable_amount = recv_data[0][0]
|
||||
|
||||
# Payable info
|
||||
pay_account = scoped_partner.property_account_payable_id
|
||||
entry.partner_payable_account_id = pay_account
|
||||
if pay_account:
|
||||
pay_domain = expression.AND([posted_filter, [('account_id', '=', pay_account.id)]])
|
||||
pay_data = self.env['account.move.line']._read_group(
|
||||
domain=pay_domain,
|
||||
aggregates=['amount_residual:sum'],
|
||||
)
|
||||
entry.partner_payable_amount = pay_data[0][0]
|
||||
|
||||
@api.depends('flag')
|
||||
def _compute_bank_account(self):
|
||||
"""Show the bank account number on the liquidity entry only."""
|
||||
for entry in self:
|
||||
if entry.flag == 'liquidity':
|
||||
stmt_line = entry.wizard_id.st_line_id
|
||||
displayed_account = stmt_line.partner_bank_id.display_name or stmt_line.account_number
|
||||
entry.bank_account = displayed_account or None
|
||||
else:
|
||||
entry.bank_account = None
|
||||
|
||||
@api.depends('wizard_id.form_index', 'amount_currency', 'balance')
|
||||
def _compute_suggestion(self):
|
||||
"""Build contextual suggestion text for matched journal items.
|
||||
|
||||
When a 'new_aml' entry is being edited, this generates guidance text
|
||||
explaining the reconciliation impact and offering a quick action button
|
||||
for full or partial matching.
|
||||
"""
|
||||
for entry in self:
|
||||
entry.suggestion_html = None
|
||||
entry.suggestion_amount_currency = None
|
||||
entry.suggestion_balance = None
|
||||
|
||||
# Only generate suggestions for actively edited matched entries
|
||||
if entry.flag != 'new_aml' or entry.index != entry.wizard_id.form_index:
|
||||
continue
|
||||
|
||||
source_item = entry.source_aml_id
|
||||
parent_widget = entry.wizard_id
|
||||
original_residual = abs(source_item.amount_residual_currency)
|
||||
post_match_residual = abs(source_item.amount_residual_currency + entry.amount_currency)
|
||||
matched_portion = original_residual - post_match_residual
|
||||
fully_consumed = source_item.currency_id.is_zero(post_match_residual)
|
||||
belongs_to_invoice = source_item.move_id.is_invoice(include_receipts=True)
|
||||
|
||||
# Build the clickable document reference
|
||||
doc_link_html = markupsafe.Markup(
|
||||
'<button name="action_redirect_to_move"'
|
||||
' class="btn btn-link p-0 align-baseline fst-italic">'
|
||||
'%(doc_name)s</button>'
|
||||
) % {'doc_name': source_item.move_id.display_name}
|
||||
|
||||
# Shared template parameters
|
||||
tpl_params = {
|
||||
'amount': formatLang(self.env, matched_portion, currency_obj=source_item.currency_id),
|
||||
'open_amount': formatLang(self.env, original_residual, currency_obj=source_item.currency_id),
|
||||
'display_name_html': doc_link_html,
|
||||
'btn_start': markupsafe.Markup(
|
||||
'<button name="action_apply_line_suggestion"'
|
||||
' class="btn btn-link p-0 align-baseline fst-italic">'
|
||||
),
|
||||
'btn_end': markupsafe.Markup('</button>'),
|
||||
}
|
||||
|
||||
if fully_consumed:
|
||||
# Full match scenario
|
||||
if belongs_to_invoice:
|
||||
status_msg = _(
|
||||
"The invoice %(display_name_html)s with an open amount of"
|
||||
" %(open_amount)s will be entirely paid by the transaction."
|
||||
)
|
||||
else:
|
||||
status_msg = _(
|
||||
"%(display_name_html)s with an open amount of %(open_amount)s"
|
||||
" will be fully reconciled by the transaction."
|
||||
)
|
||||
suggestion_lines = [status_msg]
|
||||
|
||||
# Check if a partial would be more appropriate
|
||||
partial_data = parent_widget._lines_check_partial_amount(entry)
|
||||
if partial_data:
|
||||
if belongs_to_invoice:
|
||||
partial_msg = _(
|
||||
"You might want to record a"
|
||||
" %(btn_start)spartial payment%(btn_end)s."
|
||||
)
|
||||
else:
|
||||
partial_msg = _(
|
||||
"You might want to make a"
|
||||
" %(btn_start)spartial reconciliation%(btn_end)s instead."
|
||||
)
|
||||
suggestion_lines.append(partial_msg)
|
||||
entry.suggestion_amount_currency = partial_data['amount_currency']
|
||||
entry.suggestion_balance = partial_data['balance']
|
||||
else:
|
||||
# Partial match scenario - suggest full reconciliation
|
||||
if belongs_to_invoice:
|
||||
suggestion_lines = [
|
||||
_(
|
||||
"The invoice %(display_name_html)s with an open amount of"
|
||||
" %(open_amount)s will be reduced by %(amount)s."
|
||||
),
|
||||
_(
|
||||
"You might want to set the invoice as"
|
||||
" %(btn_start)sfully paid%(btn_end)s."
|
||||
),
|
||||
]
|
||||
else:
|
||||
suggestion_lines = [
|
||||
_(
|
||||
"%(display_name_html)s with an open amount of"
|
||||
" %(open_amount)s will be reduced by %(amount)s."
|
||||
),
|
||||
_(
|
||||
"You might want to %(btn_start)sfully reconcile%(btn_end)s"
|
||||
" the document."
|
||||
),
|
||||
]
|
||||
entry.suggestion_amount_currency = entry.source_amount_currency
|
||||
entry.suggestion_balance = entry.source_balance
|
||||
|
||||
rendered_lines = markupsafe.Markup('<br/>').join(
|
||||
msg % tpl_params for msg in suggestion_lines
|
||||
)
|
||||
entry.suggestion_html = (
|
||||
markupsafe.Markup('<div class="text-muted">%s</div>') % rendered_lines
|
||||
)
|
||||
|
||||
@api.depends('flag')
|
||||
def _compute_ref_narration(self):
|
||||
"""Populate ref and narration from the statement line for liquidity entries."""
|
||||
for entry in self:
|
||||
if entry.flag == 'liquidity':
|
||||
entry.ref = entry.wizard_id.st_line_id.ref
|
||||
entry.narration = entry.wizard_id.st_line_id.narration
|
||||
else:
|
||||
entry.ref = None
|
||||
entry.narration = None
|
||||
|
||||
# =========================================================================
|
||||
# HELPERS
|
||||
# =========================================================================
|
||||
|
||||
def _get_aml_values(self, **kwargs):
|
||||
"""Convert this widget entry into values suitable for creating journal items.
|
||||
|
||||
Returns a dictionary of field values that can be passed to
|
||||
Command.create() for account.move.line records during validation.
|
||||
"""
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
'name': self.name,
|
||||
'account_id': self.account_id.id,
|
||||
'currency_id': self.currency_id.id,
|
||||
'amount_currency': self.amount_currency,
|
||||
'balance': self.debit - self.credit,
|
||||
'reconcile_model_id': self.reconcile_model_id.id,
|
||||
'analytic_distribution': self.analytic_distribution,
|
||||
'tax_repartition_line_id': self.tax_repartition_line_id.id,
|
||||
'tax_ids': [Command.set(self.tax_ids.ids)],
|
||||
'tax_tag_ids': [Command.set(self.tax_tag_ids.ids)],
|
||||
'group_tax_id': self.group_tax_id.id,
|
||||
}
|
||||
vals.update(kwargs)
|
||||
if self.flag == 'early_payment':
|
||||
vals['display_type'] = 'epd'
|
||||
return vals
|
||||
Reference in New Issue
Block a user