Files
odoo_source/addons/account_check_printing/models/account_payment.py
T
Louis Wicket (wil) 0c53d28133 [IMP] *: remove "French spacing"
According to Wiktionary, French spacing is "the archaic practice (though
still current in French) of inserting a space around colons, semicolons,
question marks, and exclamation marks". This is not standard practice in
English and most languages of the world.

The purpose of this commit is to start purging the code from this typo,
as it may reflect poorly on the software for some people.

closes odoo/odoo#116167

Related: odoo/enterprise#38542
Signed-off-by: Louis Wicket (wil) <wil@odoo.com>
2023-03-24 12:50:13 +01:00

320 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.tools.misc import formatLang, format_date
INV_LINES_PER_STUB = 9
class AccountPaymentRegister(models.TransientModel):
_inherit = "account.payment.register"
@api.depends('payment_type', 'journal_id', 'partner_id')
def _compute_payment_method_line_id(self):
super()._compute_payment_method_line_id()
for record in self:
preferred = record.partner_id.with_company(record.company_id).property_payment_method_id
method_line = record.journal_id.outbound_payment_method_line_ids.filtered(
lambda l: l.payment_method_id == preferred
)
if record.payment_type == 'outbound' and method_line:
record.payment_method_line_id = method_line[0]
class AccountPayment(models.Model):
_inherit = "account.payment"
check_amount_in_words = fields.Char(
string="Amount in Words",
store=True,
compute='_compute_check_amount_in_words',
)
check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing')
check_number = fields.Char(
string="Check Number",
store=True,
readonly=True,
copy=False,
compute='_compute_check_number',
inverse='_inverse_check_number',
help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers "
"or if the current numbering is wrong, you can change it in the journal configuration page.",
)
payment_method_line_id = fields.Many2one(index=True)
show_check_number = fields.Boolean(compute='_compute_show_check_number')
@api.depends('payment_method_line_id.code', 'check_number')
def _compute_show_check_number(self):
for payment in self:
payment.show_check_number = (
payment.payment_method_line_id.code == 'check_printing'
and payment.check_number
)
@api.constrains('check_number', 'journal_id')
def _constrains_check_number(self):
payment_checks = self.filtered('check_number')
if not payment_checks:
return
for payment_check in payment_checks:
if not payment_check.check_number.isdecimal():
raise ValidationError(_('Check numbers can only consist of digits'))
self.env.flush_all()
self.env.cr.execute("""
SELECT payment.check_number, move.journal_id
FROM account_payment payment
JOIN account_move move ON move.id = payment.move_id
JOIN account_journal journal ON journal.id = move.journal_id,
account_payment other_payment
JOIN account_move other_move ON other_move.id = other_payment.move_id
WHERE payment.check_number::BIGINT = other_payment.check_number::BIGINT
AND move.journal_id = other_move.journal_id
AND payment.id != other_payment.id
AND payment.id IN %(ids)s
AND move.state = 'posted'
AND other_move.state = 'posted'
AND payment.check_number IS NOT NULL
AND other_payment.check_number IS NOT NULL
""", {
'ids': tuple(payment_checks.ids),
})
res = self.env.cr.dictfetchall()
if res:
raise ValidationError(_(
'The following numbers are already used:\n%s',
'\n'.join(_(
'%(number)s in journal %(journal)s',
number=r['check_number'],
journal=self.env['account.journal'].browse(r['journal_id']).display_name,
) for r in res)
))
@api.depends('payment_method_line_id', 'currency_id', 'amount')
def _compute_check_amount_in_words(self):
for pay in self:
if pay.currency_id:
pay.check_amount_in_words = pay.currency_id.amount_to_text(pay.amount)
else:
pay.check_amount_in_words = False
@api.depends('journal_id', 'payment_method_code')
def _compute_check_number(self):
for pay in self:
if pay.journal_id.check_manual_sequencing and pay.payment_method_code == 'check_printing':
sequence = pay.journal_id.check_sequence_id
pay.check_number = sequence.get_next_char(sequence.number_next_actual)
else:
pay.check_number = False
def _inverse_check_number(self):
for payment in self:
if payment.check_number:
sequence = payment.journal_id.check_sequence_id.sudo()
sequence.padding = len(payment.check_number)
@api.depends('payment_type', 'journal_id', 'partner_id')
def _compute_payment_method_line_id(self):
super()._compute_payment_method_line_id()
for record in self:
preferred = record.partner_id.with_company(record.company_id).property_payment_method_id
method_line = record.journal_id.outbound_payment_method_line_ids\
.filtered(lambda l: l.payment_method_id == preferred)
if record.payment_type == 'outbound' and method_line:
record.payment_method_line_id = method_line[0]
def _get_aml_default_display_name_list(self):
# Extends 'account'
values = super()._get_aml_default_display_name_list()
if self.check_number:
date_index = [i for i, value in enumerate(values) if value[0] == 'date'][0]
values.insert(date_index - 1, ('check_number', self.check_number))
values.insert(date_index - 1, ('sep', ' - '))
return values
def action_post(self):
payment_method_check = self.env.ref('account_check_printing.account_payment_method_check')
for payment in self.filtered(lambda p: p.payment_method_id == payment_method_check and p.check_manual_sequencing):
sequence = payment.journal_id.check_sequence_id
payment.check_number = sequence.next_by_id()
return super(AccountPayment, self).action_post()
def print_checks(self):
""" Check that the recordset is valid, set the payments state to sent and call print_checks() """
# Since this method can be called via a client_action_multi, we need to make sure the received records are what we expect
self = self.filtered(lambda r: r.payment_method_line_id.code == 'check_printing' and r.state != 'reconciled')
if len(self) == 0:
raise UserError(_("Payments to print as a checks must have 'Check' selected as payment method and "
"not have already been reconciled"))
if any(payment.journal_id != self[0].journal_id for payment in self):
raise UserError(_("In order to print multiple checks at once, they must belong to the same bank journal."))
if not self[0].journal_id.check_manual_sequencing:
# The wizard asks for the number printed on the first pre-printed check
# so payments are attributed the number of the check the'll be printed on.
self.env.cr.execute("""
SELECT payment.id
FROM account_payment payment
JOIN account_move move ON movE.id = payment.move_id
WHERE journal_id = %(journal_id)s
AND payment.check_number IS NOT NULL
ORDER BY payment.check_number::BIGINT DESC
LIMIT 1
""", {
'journal_id': self.journal_id.id,
})
last_printed_check = self.browse(self.env.cr.fetchone())
number_len = len(last_printed_check.check_number or "")
next_check_number = '%0{}d'.format(number_len) % (int(last_printed_check.check_number) + 1)
return {
'name': _('Print Pre-numbered Checks'),
'type': 'ir.actions.act_window',
'res_model': 'print.prenumbered.checks',
'view_mode': 'form',
'target': 'new',
'context': {
'payment_ids': self.ids,
'default_next_check_number': next_check_number,
}
}
else:
self.filtered(lambda r: r.state == 'draft').action_post()
return self.do_print_checks()
def action_unmark_sent(self):
self.write({'is_move_sent': False})
def action_void_check(self):
self.action_draft()
self.action_cancel()
def do_print_checks(self):
check_layout = self.company_id.account_check_printing_layout
redirect_action = self.env.ref('account.action_account_config')
if not check_layout or check_layout == 'disabled':
msg = _("You have to choose a check layout. For this, go in Invoicing/Accounting Settings, search for 'Checks layout' and set one.")
raise RedirectWarning(msg, redirect_action.id, _('Go to the configuration panel'))
report_action = self.env.ref(check_layout, False)
if not report_action:
msg = _("Something went wrong with Check Layout, please select another layout in Invoicing/Accounting Settings and try again.")
raise RedirectWarning(msg, redirect_action.id, _('Go to the configuration panel'))
self.write({'is_move_sent': True})
return report_action.report_action(self)
#######################
#CHECK PRINTING METHODS
#######################
def _check_fill_line(self, amount_str):
return amount_str and (amount_str + ' ').ljust(200, '*') or ''
def _check_build_page_info(self, i, p):
multi_stub = self.company_id.account_check_printing_multi_stub
return {
'sequence_number': self.check_number,
'manual_sequencing': self.journal_id.check_manual_sequencing,
'date': format_date(self.env, self.date),
'partner_id': self.partner_id,
'partner_name': self.partner_id.name,
'currency': self.currency_id,
'state': self.state,
'amount': formatLang(self.env, self.amount, currency_obj=self.currency_id) if i == 0 else 'VOID',
'amount_in_word': self._check_fill_line(self.check_amount_in_words) if i == 0 else 'VOID',
'memo': self.ref,
'stub_cropped': not multi_stub and len(self.move_id._get_reconciled_invoices()) > INV_LINES_PER_STUB,
# If the payment does not reference an invoice, there is no stub line to display
'stub_lines': p,
}
def _check_get_pages(self):
""" Returns the data structure used by the template: a list of dicts containing what to print on pages.
"""
stub_pages = self._check_make_stub_pages() or [False]
pages = []
for i, p in enumerate(stub_pages):
pages.append(self._check_build_page_info(i, p))
return pages
def _check_make_stub_pages(self):
""" The stub is the summary of paid invoices. It may spill on several pages, in which case only the check on
first page is valid. This function returns a list of stub lines per page.
"""
self.ensure_one()
def prepare_vals(invoice, partials):
number = ' - '.join([invoice.name, invoice.ref] if invoice.ref else [invoice.name])
if invoice.is_outbound() or invoice.move_type == 'in_receipt':
invoice_sign = 1
partial_field = 'debit_amount_currency'
else:
invoice_sign = -1
partial_field = 'credit_amount_currency'
if invoice.currency_id.is_zero(invoice.amount_residual):
amount_residual_str = '-'
else:
amount_residual_str = formatLang(self.env, invoice_sign * invoice.amount_residual, currency_obj=invoice.currency_id)
return {
'due_date': format_date(self.env, invoice.invoice_date_due),
'number': number,
'amount_total': formatLang(self.env, invoice_sign * invoice.amount_total, currency_obj=invoice.currency_id),
'amount_residual': amount_residual_str,
'amount_paid': formatLang(self.env, invoice_sign * sum(partials.mapped(partial_field)), currency_obj=self.currency_id),
'currency': invoice.currency_id,
}
# Decode the reconciliation to keep only invoices.
term_lines = self.line_ids.filtered(lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable'))
invoices = (term_lines.matched_debit_ids.debit_move_id.move_id + term_lines.matched_credit_ids.credit_move_id.move_id)\
.filtered(lambda x: x.is_outbound() or x.move_type == 'in_receipt')
invoices = invoices.sorted(lambda x: x.invoice_date_due or x.date)
# Group partials by invoices.
invoice_map = {invoice: self.env['account.partial.reconcile'] for invoice in invoices}
for partial in term_lines.matched_debit_ids:
invoice = partial.debit_move_id.move_id
if invoice in invoice_map:
invoice_map[invoice] |= partial
for partial in term_lines.matched_credit_ids:
invoice = partial.credit_move_id.move_id
if invoice in invoice_map:
invoice_map[invoice] |= partial
# Prepare stub_lines.
if 'out_refund' in invoices.mapped('move_type'):
stub_lines = [{'header': True, 'name': "Bills"}]
stub_lines += [prepare_vals(invoice, partials)
for invoice, partials in invoice_map.items()
if invoice.move_type == 'in_invoice']
stub_lines += [{'header': True, 'name': "Refunds"}]
stub_lines += [prepare_vals(invoice, partials)
for invoice, partials in invoice_map.items()
if invoice.move_type == 'out_refund']
else:
stub_lines = [prepare_vals(invoice, partials)
for invoice, partials in invoice_map.items()
if invoice.move_type in ('in_invoice', 'in_receipt')]
# Crop the stub lines or split them on multiple pages
if not self.company_id.account_check_printing_multi_stub:
# If we need to crop the stub, leave place for an ellipsis line
num_stub_lines = len(stub_lines) > INV_LINES_PER_STUB and INV_LINES_PER_STUB - 1 or INV_LINES_PER_STUB
stub_pages = [stub_lines[:num_stub_lines]]
else:
stub_pages = []
i = 0
while i < len(stub_lines):
# Make sure we don't start the credit section at the end of a page
if len(stub_lines) >= i + INV_LINES_PER_STUB and stub_lines[i + INV_LINES_PER_STUB - 1].get('header'):
num_stub_lines = INV_LINES_PER_STUB - 1 or INV_LINES_PER_STUB
else:
num_stub_lines = INV_LINES_PER_STUB
stub_pages.append(stub_lines[i:i + num_stub_lines])
i += num_stub_lines
return stub_pages