import calendar
from contextlib import contextmanager
from itertools import chain
from dateutil.relativedelta import relativedelta
import logging
import markupsafe
import re
from odoo import fields, models, api, _
from odoo.exceptions import UserError
from odoo.fields import Command, Domain
from odoo.tools import SQL, float_compare
from odoo.tools.misc import formatLang
_logger = logging.getLogger(__name__)
DEFERRED_DATE_MIN = '1900-01-01'
DEFERRED_DATE_MAX = '9999-12-31'
class AccountMove(models.Model):
_inherit = "account.move"
# Technical field to keep the value of payment_state when switching from invoicing to accounting
# (using invoicing_switch_threshold setting field). It allows keeping the former payment state, so that
# we can restore it if the user misconfigured the switch date and wants to change it.
payment_state_before_switch = fields.Char(string="Payment State Before Switch", copy=False)
# Deferred management fields
deferred_move_ids = fields.Many2many(
string="Deferred Entries",
comodel_name='account.move',
relation='account_move_deferred_rel',
column1='original_move_id',
column2='deferred_move_id',
help="The deferred entries created by this invoice",
copy=False,
)
deferred_original_move_ids = fields.Many2many(
string="Original Invoices",
comodel_name='account.move',
relation='account_move_deferred_rel',
column1='deferred_move_id',
column2='original_move_id',
help="The original invoices that created the deferred entries",
copy=False,
)
deferred_entry_type = fields.Selection(
string="Deferred Entry Type",
selection=[
('expense', 'Deferred Expense'),
('revenue', 'Deferred Revenue'),
('misc', 'Deferred Miscellaneous'),
],
compute='_compute_deferred_entry_type',
copy=False,
)
signing_user = fields.Many2one(
string='Signer',
comodel_name='res.users',
compute='_compute_signing_user', store=True,
copy=False,
)
show_signature_area = fields.Boolean(compute='_compute_signature')
signature = fields.Binary(compute='_compute_signature') # can't be `related`: the sign module might not be there
@api.depends('state', 'move_type', 'invoice_user_id')
def _compute_signing_user(self):
other_moves = self.filtered(lambda move: not move.is_sale_document())
other_moves.signing_user = False
is_odoobot_user = self.env.user == self.env.ref('base.user_root')
is_backend_user = self.env.user.has_group('base.group_user')
for invoice in (self - other_moves).filtered(lambda inv: inv.state == 'posted'):
# signer priority:
# - res.user set in res.settings
# - real backend user posting the invoice
# - if odoobot: the person that initiated the invoice ie: The salesman
# - if invoice initiated by a portal user -> No signature
representative = invoice.company_id.signing_user
# checking `has_group('base.group_user')` ensure we never keep a portal user to sign
if is_odoobot_user:
user_can_sign = invoice.invoice_user_id and invoice.invoice_user_id.has_group('base.group_user')
invoice.signing_user = representative or invoice.invoice_user_id if user_can_sign else False
else:
invoice.signing_user = representative or self.env.user if is_backend_user else False
@api.depends('state')
def _compute_signature(self):
is_portal_user = self.env.user.has_group('base.group_portal')
# Checking `company_id.sign_invoice` removes the needs to check if the sign module is installed
# Setting it to True through `res.settings` auto install the sign module
moves_not_to_sign = self.filtered(
lambda inv: not inv.company_id.sign_invoice
or inv.state in {'draft', 'cancel'}
or not inv.is_sale_document()
# Allow signature for portal user only if the invoice already went through the send&print workflow
or (is_portal_user and not inv.invoice_pdf_report_id)
)
moves_not_to_sign.show_signature_area = False
moves_not_to_sign.signature = None
invoice_with_signature = self - moves_not_to_sign
invoice_with_signature.show_signature_area = True
for invoice in invoice_with_signature:
invoice.signature = invoice.signing_user.sudo().sign_signature
def _post(self, soft=True):
# Deferred management
posted = super()._post(soft)
for move in self:
if move._get_deferred_entries_method() == 'on_validation' and any(move.line_ids.mapped('deferred_start_date')):
move._generate_deferred_entries()
return posted
def button_draft(self):
if any(len(deferral_move.deferred_original_move_ids) > 1 for deferral_move in self.deferred_move_ids):
raise UserError(_("You cannot reset to draft an invoice that is grouped in deferral entry. You can create a credit note instead."))
reversed_moves = self.deferred_move_ids._unlink_or_reverse()
if reversed_moves:
for move in reversed_moves:
move.with_context(skip_readonly_check=True).write({
'date': move._get_accounting_date(move.date, move._affect_tax_report()),
})
self.deferred_move_ids |= reversed_moves
return super().button_draft()
def unlink(self):
# Prevent deferred moves under audit trail restriction from being unlinked
deferral_moves = self.filtered(lambda move: move._is_protected_by_audit_trail() and move.deferred_original_move_ids)
deferral_moves.deferred_original_move_ids.deferred_move_ids = False
deferral_moves._reverse_moves()
return super(AccountMove, self - deferral_moves).unlink()
# ============================= START - Deferred Management ====================================
def _get_deferred_entries_method(self):
self.ensure_one()
if self.is_entry():
move_types = set(self.line_ids.filtered('deferred_start_date').account_id.mapped("internal_group"))
if (
"expense" in move_types and "income" in move_types
and self.company_id.generate_deferred_expense_entries_method != self.company_id.generate_deferred_revenue_entries_method
):
raise UserError(self.env._(
"Having different deferred entries generation methods for expenses and revenues is not supported on "
"journal entries involving both expense and revenue accounts. You can split this entry into two entries instead."
))
elif "expense" in move_types:
return self.company_id.generate_deferred_expense_entries_method
else:
return self.company_id.generate_deferred_revenue_entries_method
if self.is_purchase_document():
return self.company_id.generate_deferred_expense_entries_method
return self.company_id.generate_deferred_revenue_entries_method
@api.depends('deferred_original_move_ids')
def _compute_deferred_entry_type(self):
for move in self:
if move.deferred_original_move_ids:
move_types = set(move.deferred_original_move_ids.mapped('move_type'))
if len(move_types) > 1 or move.deferred_original_move_ids[0].is_entry():
move.deferred_entry_type = 'misc'
elif move.deferred_original_move_ids[0].is_purchase_document():
move.deferred_entry_type = 'expense'
else:
move.deferred_entry_type = 'revenue'
else:
move.deferred_entry_type = False
@api.model
def _get_deferred_diff_dates(self, start, end):
"""
Returns the number of months between two dates [start, end[
The computation is done by using months of 30 days so that the deferred amount for february
(28-29 days), march (31 days) and april (30 days) are all the same (in case of monthly computation).
See test_deferred_management_get_diff_dates for examples.
"""
if start > end:
start, end = end, start
nb_months = end.month - start.month + 12 * (end.year - start.year)
start_day, end_day = start.day, end.day
if start_day == calendar.monthrange(start.year, start.month)[1]:
start_day = 30
if end_day == calendar.monthrange(end.year, end.month)[1]:
end_day = 30
nb_days = end_day - start_day
return (nb_months * 30 + nb_days) / 30
@api.model
def _get_deferred_period_amount(self, method, period_start, period_end, line_start, line_end, balance):
"""
Returns the amount to defer for the given period taking into account the deferred method (day/month/full_months).
"""
if period_end <= line_start or period_end <= period_start:
return 0 # invalid period
if method == 'day':
amount_per_day = balance / (line_end - line_start).days
return (period_end - period_start).days * amount_per_day
elif method in ('month', 'full_months'):
if method == 'full_months':
reset_day_1 = relativedelta(day=1)
line_start, line_end = line_start + reset_day_1, line_end + reset_day_1
period_start, period_end = period_start + reset_day_1, period_end + reset_day_1
line_diff = self._get_deferred_diff_dates(line_end, line_start)
period_diff = self._get_deferred_diff_dates(period_end, period_start)
return period_diff / line_diff * balance if line_diff else balance
@api.model
def _get_deferred_amounts_by_line(self, lines, periods, deferred_type):
"""
:return: a list of dictionaries containing the deferred amounts for each line and each period
E.g. (where period1 = (date1, date2, label1), period2 = (date2, date3, label2), ...)
[
{'account_id': 1, period_1: 100, period_2: 200},
{'account_id': 1, period_1: 100, period_2: 200},
{'account_id': 2, period_1: 300, period_2: 400},
]
"""
values = []
for line in lines:
line_start = fields.Date.to_date(line['deferred_start_date'])
line_end = fields.Date.to_date(line['deferred_end_date'])
if line_end < line_start:
# This normally shouldn't happen, but if it does, would cause calculation errors later on.
# To not make the reports crash, we just set both dates to the same day.
# The user should fix the dates manually.
line_end = line_start
columns = {}
for period in periods:
if period[2] == 'not_started' and line_start <= period[0]:
# The 'Not Started' column only considers lines starting the deferral after the report end date
columns[period] = 0.0
continue
# periods = [Total, Not Started, Before, ..., Current, ..., Later]
# The dates to calculate the amount for the current period
period_start = max(period[0], line_start)
period_end = min(period[1], line_end) + relativedelta(days=1) # +1 to include end date of report
columns[period] = self._get_deferred_period_amount(
self.env.company.deferred_expense_amount_computation_method if deferred_type == "expense" else self.env.company.deferred_revenue_amount_computation_method,
period_start, period_end,
line_start, line_end + relativedelta(days=1), # +1 to include the end date of the line
line['balance']
)
values.append({
**self.env['account.move.line']._get_deferred_amounts_by_line_values(line),
**columns,
})
return values
@api.model
def _get_deferred_lines(self, line, deferred_account, deferred_type, period, ref, force_balance=None, grouping_field='account_id'):
"""
:return: a list of Command objects to create the deferred lines of a single given period
"""
deferred_amounts = self._get_deferred_amounts_by_line(line, [period], deferred_type)[0]
balance = deferred_amounts[period] if force_balance is None else force_balance
return [
Command.create({
**self.env['account.move.line']._get_deferred_lines_values(account.id, coeff * balance, ref, line.analytic_distribution, line),
'partner_id': line.partner_id.id,
'product_id': line.product_id.id,
})
for (account, coeff) in [(deferred_amounts[grouping_field], 1), (deferred_account, -1)]
]
def _generate_deferred_entries(self):
"""
Generates the deferred entries for the invoice.
"""
self.ensure_one()
if self.state != 'posted':
return
for deferred_type in ('expense', 'revenue'):
moves_vals_to_create = []
lines_vals_to_create = []
lines_periods = []
lines = self.line_ids.filtered(lambda l: (
l.account_id.internal_group == ('expense' if deferred_type == 'expense' else 'income')
and l.deferred_start_date
and l.deferred_end_date
))
if not lines:
continue
deferred_account = self.company_id.deferred_expense_account_id if deferred_type == "expense" else self.company_id.deferred_revenue_account_id
deferred_journal = self.company_id.deferred_expense_journal_id if deferred_type == "expense" else self.company_id.deferred_revenue_journal_id
deferred_method = self.company_id.deferred_expense_amount_computation_method if deferred_type == "expense" else self.company_id.deferred_revenue_amount_computation_method
if not deferred_journal:
raise UserError(_("Please set the deferred journal in the accounting settings."))
if not deferred_account:
raise UserError(_("Please set the deferred accounts in the accounting settings."))
for line in lines:
periods = line._get_deferred_periods()
if not periods:
continue
start_date = line.deferred_start_date
end_date = line.deferred_end_date
accounting_date = line.date
# When using the 'full_months' computation method, every consumed month counts as a full month.
# We therefore need to subtract one month from the end date for the following check on dates.
if deferred_method == 'full_months':
# We need to add one day to the end date since it's excluded by _get_deferred_diff_dates().
if self._get_deferred_diff_dates(start_date.replace(day=1), end_date + relativedelta(days=1)) < 2:
end_date += relativedelta(months=-1)
# When all move line dates (start, end, accounting) are within the same month, we skip the line.
# It would otherwise lead to the creation of both a reversal and a deferral move that would cancel each other out.
if start_date.replace(day=1) == end_date.replace(day=1) == accounting_date.replace(day=1):
continue
ref = _("Deferral of %s", line.move_id.name or '')
moves_vals_to_create.append({
'move_type': 'entry',
'deferred_original_move_ids': [Command.set(line.move_id.ids)],
'journal_id': deferred_journal.id,
'company_id': self.company_id.id,
'partner_id': line.partner_id.id,
'auto_post': 'at_date',
'ref': ref,
'name': False,
'date': line.move_id.date,
})
lines_vals_to_create.append([
{
**self.env['account.move.line']._get_deferred_lines_values(account.id, coeff * line.balance, ref, line.analytic_distribution, line),
'partner_id': line.partner_id.id,
}
for (account, coeff) in [(line.account_id, -1), (deferred_account, 1)]
])
lines_periods.append((line, periods))
# create the deferred moves
moves_fully_deferred = self.create(moves_vals_to_create)
# We write the lines after creation, to make sure the `deferred_original_move_ids` is set.
# This way we can avoid adding taxes for deferred moves.
for move_fully_deferred, lines_vals in zip(moves_fully_deferred, lines_vals_to_create):
for line_vals in lines_vals:
# This will link the moves to the lines. Instead of move.write('line_ids': lines_ids)
line_vals['move_id'] = move_fully_deferred.id
self.env['account.move.line'].create(list(chain(*lines_vals_to_create)))
deferral_moves_vals = []
deferral_moves_line_vals = []
# Create the deferred entries for the periods [deferred_start_date, deferred_end_date]
for (line, periods), move_vals in zip(lines_periods, moves_vals_to_create):
remaining_balance = line.balance
for period_index, period in enumerate(periods):
# For the last deferral move the balance is forced to remaining balance to avoid rounding errors
force_balance = remaining_balance if period_index == len(periods) - 1 else None
deferred_amounts = self._get_deferred_amounts_by_line(line, [period], deferred_type)[0]
balance = deferred_amounts[period] if force_balance is None else force_balance
remaining_balance -= line.currency_id.round(balance)
deferral_moves_vals.append({**move_vals, 'date': period[1]})
deferral_moves_line_vals.append([
{
**self.env['account.move.line']._get_deferred_lines_values(account.id, coeff * balance, move_vals['ref'], line.analytic_distribution, line),
'partner_id': line.partner_id.id,
'product_id': line.product_id.id,
}
for (account, coeff) in [(deferred_amounts['account_id'], 1), (deferred_account, -1)]
])
deferral_moves = self.create(deferral_moves_vals)
for deferral_move, lines_vals in zip(deferral_moves, deferral_moves_line_vals):
for line_vals in lines_vals:
# This will link the moves to the lines. Instead of move.write('line_ids': lines_ids)
line_vals['move_id'] = deferral_move.id
self.env['account.move.line'].create(list(chain(*deferral_moves_line_vals)))
# Avoid having deferral moves with a total amount of 0.
to_unlink = deferral_moves.filtered(lambda move: move.currency_id.is_zero(move.amount_total))
to_unlink.unlink()
(moves_fully_deferred + deferral_moves - to_unlink)._post(soft=True)
def open_deferred_entries(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _("Deferred Entries"),
'res_model': 'account.move.line',
'domain': [('id', 'in', self.deferred_move_ids.line_ids.ids)],
'views': [(self.env.ref('account_accountant.view_deferred_entries_tree').id, 'list')],
'context': {
'search_default_group_by_move': True,
'expand': True,
}
}
def open_deferred_original_entry(self):
self.ensure_one()
action = {
'type': 'ir.actions.act_window',
'name': _("Original Deferred Entries"),
'res_model': 'account.move.line',
'domain': [('id', 'in', self.deferred_original_move_ids.line_ids.ids)],
'views': [(False, 'list'), (False, 'form')],
'context': {
'search_default_group_by_move': True,
'expand': True,
}
}
if len(self.deferred_original_move_ids) == 1:
action.update({
'res_model': 'account.move',
'res_id': self.deferred_original_move_ids[0].id,
'views': [(False, 'form')],
})
return action
# ============================= END - Deferred management ======================================
def action_open_bank_reconciliation_widget(self):
return self.statement_line_id._action_open_bank_reconciliation_widget(
default_context={
'search_default_journal_id': self.statement_line_id.journal_id.id,
'search_default_statement_line_id': self.statement_line_id.id,
'default_st_line_id': self.statement_line_id.id,
}
)
def action_open_bank_reconciliation_widget_statement(self):
return self.statement_line_id._action_open_bank_reconciliation_widget(
extra_domain=[('statement_id', 'in', self.statement_id.ids)],
)
def action_open_business_doc(self):
if self.statement_line_id:
return self.action_open_bank_reconciliation_widget()
else:
action = super().action_open_business_doc()
# prevent propagation of the following keys
action['context'] = action.get('context', {}) | {
'preferred_aml_value': None,
'preferred_aml_currency_id': None,
}
return action
def _get_mail_thread_data_attachments(self):
res = super()._get_mail_thread_data_attachments()
res += self.statement_line_id.statement_id.attachment_ids
if self.env.context.get('from_bank_reco'):
res += self.line_ids.reconciled_lines_ids.move_attachment_ids
return res
@contextmanager
def _get_edi_creation(self):
with super()._get_edi_creation() as move:
previous_lines = move.invoice_line_ids
yield move.with_context(disable_onchange_name_predictive=True)
for line in move.invoice_line_ids - previous_lines:
line._onchange_name_predictive()
def _get_outstanding_bank_statement_lines_domain(self):
"""
Returns a search domain for move lines for unreconciled bank payments
linked to this move's partner.
"""
self.ensure_one()
return [
('parent_state', '=', 'posted'),
('partner_id', '=', self.commercial_partner_id.id),
('partner_id', '!=', False),
('account_id.account_type', '=', 'asset_cash'),
('journal_id', 'in', self.env['account.journal']._search([
*self.env['account.journal']._check_company_domain(self.company_id.id),
('type', '=', 'bank')
])),
('balance', '>' if self.is_inbound() else '<', 0.0),
('statement_line_id', '!=', False),
('move_id.line_ids', 'any', [
('account_id', '=', self.company_id.account_journal_suspense_account_id.id),
('reconciled', '=', False),
]),
]
def _compute_payments_widget_to_reconcile_info(self):
# EXTENDS 'account'
super()._compute_payments_widget_to_reconcile_info()
for move in self:
if move.state not in {'draft', 'posted'} \
or move.payment_state not in ('not_paid', 'partial') \
or not move.is_invoice(include_receipts=True) \
or not move.partner_id:
continue
bank_domain = [
('parent_state', '=', 'posted'),
('partner_id', '=', move.commercial_partner_id.id),
('partner_id', '!=', False),
('account_id.account_type', '=', 'asset_cash'),
('journal_id', 'in', self.env['account.journal']._search([
*self.env['account.journal']._check_company_domain(move.company_id.id),
('type', '=', 'bank')
])),
('balance', '>' if move.is_inbound() else '<', 0.0),
('statement_line_id', '!=', False),
('move_id.line_ids', 'any', [
('account_id', '=', move.company_id.account_journal_suspense_account_id.id),
('reconciled', '=', False),
]),
]
payments_widget_vals = {
'outstanding': True,
'content': [],
'move_id': move.id,
'title': _('Outstanding credits') if move.is_inbound() else _('Outstanding debits')
}
for line in self.env['account.move.line'].search(bank_domain):
st_line = line.statement_line_id
currency = st_line.foreign_currency_id or st_line.currency_id
amount = currency._convert(
from_amount=abs(st_line.amount_residual),
to_currency=move.currency_id,
company=move.company_id,
date=line.date,
)
if move.currency_id.is_zero(amount):
continue
payments_widget_vals['content'].append({
'bank_label': line.name if line.journal_id.type == 'bank' else False,
'journal_name': line.ref or line.move_id.name,
'amount': amount,
'currency_id': move.currency_id.id,
'id': line.id,
'move_id': line.move_id.id,
'date': fields.Date.to_string(line.date),
'account_payment_id': line.payment_id.id,
})
if payments_widget_vals['content']:
if move.invoice_outstanding_credits_debits_widget:
move.invoice_outstanding_credits_debits_widget['content'].extend(payments_widget_vals['content'])
move.invoice_outstanding_credits_debits_widget['content'].sort(key=lambda x: (x['date'], x['id']), reverse=True)
else:
move.invoice_outstanding_credits_debits_widget = payments_widget_vals
def _get_partner_credit_warning_exclude_amount(self):
# EXTENDS 'account'
exclude_amount = super()._get_partner_credit_warning_exclude_amount()
bank_domain = self._get_outstanding_bank_statement_lines_domain()
for line in self.env['account.move.line'].search(bank_domain):
currency = line.statement_line_id.foreign_currency_id or line.statement_line_id.currency_id
exclude_amount += currency._convert(
from_amount=abs(line.statement_line_id.amount_residual),
to_currency=self.company_id.currency_id,
company=self.company_id,
date=line.date,
)
matched_credits = self.line_ids.filtered(lambda line: line.account_id.account_type == 'asset_receivable').matched_credit_ids
if matched_credits:
bank_line_amount = sum(matched_credits.mapped('amount'))
payment_date = max(matched_credits.mapped('max_date'))
amount_company = matched_credits[0].credit_currency_id._convert(
from_amount=bank_line_amount,
to_currency=self.company_id.currency_id,
company=self.company_id,
date=payment_date,
)
exclude_amount += amount_company
return exclude_amount
def js_assign_outstanding_line(self, line_id):
# EXTENDS
super().js_assign_outstanding_line(line_id)
line = self.env['account.move.line'].browse(line_id)
if line.account_id.account_type == 'asset_cash' and line.statement_line_id:
return line.statement_line_id.with_context(
skip_payment_tolerance=True,
stop_reco_at_first_partial=True,
).set_line_bank_statement_line(self.line_ids.filtered(lambda line: line.account_id.account_type in {'asset_receivable', 'liability_payable'}).ids)
def js_remove_outstanding_partial(self, partial_id):
# EXTENDS
if st_line := self.statement_line_id:
partial = self.env['account.partial.reconcile'].browse(partial_id)
st_line.delete_reconciled_line((partial.credit_move_id + partial.debit_move_id).ids)
else:
super().js_remove_outstanding_partial(partial_id)
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
move_attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment')
# Deferred management fields
deferred_start_date = fields.Date(
string="Start Date",
compute='_compute_deferred_start_date', store=True, readonly=False,
index='btree_not_null',
copy=False,
help="Date at which the deferred expense/revenue starts"
)
deferred_end_date = fields.Date(
string="End Date",
index='btree_not_null',
copy=False,
help="Date at which the deferred expense/revenue ends"
)
has_deferred_moves = fields.Boolean(compute='_compute_has_deferred_moves')
has_abnormal_deferred_dates = fields.Boolean(compute='_compute_has_abnormal_deferred_dates')
full_amount_switch_html = fields.Html(compute='_compute_full_amount_switch_html')
def _order_to_sql(self, order, query, alias=None, reverse=False):
sql_order = super()._order_to_sql(order, query, alias, reverse)
preferred_aml_residual_value = self.env.context.get('preferred_aml_value')
preferred_aml_currency_id = self.env.context.get('preferred_aml_currency_id')
if preferred_aml_residual_value and preferred_aml_currency_id and order == self._order:
currency = self.env['res.currency'].browse(preferred_aml_currency_id)
# using round since currency.round(55.55) = 55.550000000000004
preferred_aml_residual_value = round(preferred_aml_residual_value, currency.decimal_places)
sql_residual_currency = self._field_to_sql(alias or self._table, 'amount_residual_currency', query)
sql_currency = self._field_to_sql(alias or self._table, 'currency_id', query)
return SQL(
"ROUND(%(residual_currency)s, %(decimal_places)s) = %(value)s "
"AND %(currency)s = %(currency_id)s DESC, %(order)s",
residual_currency=sql_residual_currency,
decimal_places=currency.decimal_places,
value=preferred_aml_residual_value,
currency=sql_currency,
currency_id=currency.id,
order=sql_order,
)
return sql_order
def copy_data(self, default=None):
data_list = super().copy_data(default=default)
for line, values in zip(self, data_list):
if 'move_reverse_cancel' in self.env.context:
values['deferred_start_date'] = line.deferred_start_date
values['deferred_end_date'] = line.deferred_end_date
return data_list
def write(self, vals):
""" Prevent changing the account of a move line when there are already deferral entries.
"""
if 'account_id' in vals:
for line in self:
if (
line.has_deferred_moves
and line.deferred_start_date
and line.deferred_end_date
and vals['account_id'] != line.account_id.id
):
raise UserError(_(
"You cannot change the account for a deferred line in %(move_name)s if it has already been deferred.",
move_name=line.move_id.display_name
))
return super().write(vals)
@api.depends('balance')
def _compute_full_amount_switch_html(self):
for line in self:
# Ignore lines with no reconciled lines or not linked to a bank statement.
if not (reconciled_lines := line.reconciled_lines_excluding_exchange_diff_ids) or len(reconciled_lines) > 1 or not line.statement_line_id:
line.full_amount_switch_html = False
continue
is_invoice = reconciled_lines.move_id.is_invoice(include_receipts=True)
btn_start = markupsafe.Markup("")
if reconciled_lines.currency_id.is_zero(reconciled_lines.amount_currency + line.amount_currency):
lines = [
_("%(display_name_html)s will be entirely paid by the transaction.")
if is_invoice else
_("%(display_name_html)s will be fully reconciled by the transaction.")
]
liquidity_line_amount_currency = line.move_id.line_ids.filtered(lambda line: line.account_id == line.move_id.journal_id.default_account_id).amount_currency
# Means that we possibly want to come back to partial
if float_compare(liquidity_line_amount_currency, reconciled_lines.amount_currency, 2) < 0:
btn_start = markupsafe.Markup("")
lines.append(
_("You might want to record a %(btn_start)spartial payment%(btn_end)s.")
if is_invoice else
_("You might want to make a %(btn_start)spartial reconciliation%(btn_end)s instead.")
)
else:
if is_invoice:
lines = [
_("%(display_name_html)s will be reduced by %(amount)s."),
_("You might want to set the invoice as %(btn_start)sfully paid%(btn_end)s."),
]
else:
lines = [
_("%(display_name_html)s will be reduced by %(amount)s."),
_("You might want to %(btn_start)sfully reconcile%(btn_end)s the document."),
]
# We need to use span instead of button here because button is not displayed otherwise
display_name_html = markupsafe.Markup("""
%(display_name)s
""") % {
'display_name': reconciled_lines.move_id._get_move_display_name(show_ref=False),
}
extra_text = markupsafe.Markup("
").join(lines) % {
'amount': formatLang(self.env, line.amount_currency, currency_obj=line.currency_id),
'display_name_html': display_name_html,
'btn_start': btn_start,
'btn_end': markupsafe.Markup(""),
}
line.full_amount_switch_html = markupsafe.Markup("