19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -2,9 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_journal
from . import account_move
from . import account_payment
from . import account_payment_method
from . import res_company
from . import res_config_settings
from . import res_partner

View file

@ -4,6 +4,7 @@
import re
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
MAX_INT32 = 2147483647
class AccountJournal(models.Model):
@ -34,6 +35,16 @@ class AccountJournal(models.Model):
help="Sequence number of the next printed check.",
)
bank_check_printing_layout = fields.Selection(
selection='_get_check_printing_layouts',
string="Check Layout",
)
def _get_check_printing_layouts(self):
""" Returns available check printing layouts for the company, excluding disabled options """
selection = self.company_id._fields['account_check_printing_layout'].selection
return [(value, label) for value, label in selection if value != 'disabled']
@api.depends('check_manual_sequencing')
def _compute_check_next_number(self):
for journal in self:
@ -45,16 +56,24 @@ class AccountJournal(models.Model):
def _inverse_check_next_number(self):
for journal in self:
next_num = int(journal.check_next_number)
if journal.check_next_number and not re.match(r'^[0-9]+$', journal.check_next_number):
raise ValidationError(_('Next Check Number should only contains numbers.'))
if int(journal.check_next_number) < journal.check_sequence_id.number_next_actual:
if next_num < journal.check_sequence_id.number_next_actual:
raise ValidationError(_(
"The last check number was %s. In order to avoid a check being rejected "
"by the bank, you can only use a greater number.",
journal.check_sequence_id.number_next_actual
))
if journal.check_sequence_id:
journal.check_sequence_id.sudo().number_next_actual = int(journal.check_next_number)
if next_num > MAX_INT32:
raise ValidationError(_(
"The check number you entered (%(num)s) exceeds the maximum allowed value of %(max)d. "
"Please enter a smaller number.",
num=next_num,
max=MAX_INT32,
))
journal.check_sequence_id.sudo().number_next_actual = next_num
journal.check_sequence_id.sudo().padding = len(journal.check_next_number)
@api.model_create_multi
@ -67,7 +86,7 @@ class AccountJournal(models.Model):
""" Create a check sequence for the journal """
for journal in self:
journal.check_sequence_id = self.env['ir.sequence'].sudo().create({
'name': journal.name + _(" : Check Number Sequence"),
'name': _("%(journal)s: Check Number Sequence", journal=journal.name),
'implementation': 'no_gap',
'padding': 5,
'number_increment': 1,
@ -78,8 +97,8 @@ class AccountJournal(models.Model):
dashboard_data = super()._get_journal_dashboard_data_batched()
self._fill_dashboard_data_count(dashboard_data, 'account.payment', 'num_checks_to_print', [
('payment_method_line_id.code', '=', 'check_printing'),
('state', '=', 'posted'),
('is_move_sent','=', False),
('state', '=', 'in_process'),
('is_sent', '=', False),
])
return dashboard_data

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
from odoo.tools.sql import column_exists, create_column
class AccountMove(models.Model):
_inherit = 'account.move'
preferred_payment_method_id = fields.Many2one(
string="Preferred Payment Method",
comodel_name='account.payment.method',
compute='_compute_preferred_payment_method_idd',
store=True,
)
def _auto_init(self):
""" Create column for `preferred_payment_method_id` to avoid having it
computed by the ORM on installation. Since `property_payment_method_id` is
introduced in this module, there is no need for UPDATE
"""
if not column_exists(self.env.cr, "account_move", "preferred_payment_method_id"):
create_column(self.env.cr, "account_move", "preferred_payment_method_id", "int4")
return super()._auto_init()
@api.depends('partner_id')
def _compute_preferred_payment_method_idd(self):
for move in self:
partner = move.partner_id
# take the payment method corresponding to the move's company
move.preferred_payment_method_id = partner.with_company(move.company_id).property_payment_method_id

View file

@ -9,21 +9,6 @@ from odoo.tools.sql import column_exists, create_column
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"
@ -36,7 +21,6 @@ class AccountPayment(models.Model):
check_number = fields.Char(
string="Check Number",
store=True,
readonly=True,
copy=False,
compute='_compute_check_number',
inverse='_inverse_check_number',
@ -46,6 +30,12 @@ class AccountPayment(models.Model):
payment_method_line_id = fields.Many2one(index=True)
show_check_number = fields.Boolean(compute='_compute_show_check_number')
check_layout_available = fields.Boolean(
string='Has Check Layout',
store=False,
default=lambda self: len(self.env['res.company']._fields['account_check_printing_layout'].selection) > 1
)
@api.depends('payment_method_line_id.code', 'check_number')
def _compute_show_check_number(self):
for payment in self:
@ -128,24 +118,39 @@ class AccountPayment(models.Model):
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]
@api.model
def fields_get(self, allfields=None, attributes=None):
result = super().fields_get(allfields, attributes)
# pretend the field 'check_number' to be readonly
field_desc = result.get('check_number') or {}
if 'readonly' in field_desc:
field_desc['readonly'] = True
return result
@api.model
def _get_trigger_fields_to_synchronize(self):
return super()._get_trigger_fields_to_synchronize() + ('check_number',)
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
self.ensure_one()
if not self.check_number:
return super()._get_aml_default_display_name_list()
result = [
('label', _("Checks")),
('sep', ' - '),
('check_number', self.check_number),
]
if self.memo:
result += [
('sep', ': '),
('memo', self.memo),
]
return result
def action_post(self):
payment_method_check = self.env.ref('account_check_printing.account_payment_method_check')
@ -157,31 +162,30 @@ class AccountPayment(models.Model):
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')
valid_payments = self.filtered(lambda r: r.payment_method_line_id.code == 'check_printing' and not r.is_sent)
if len(self) == 0:
if len(valid_payments) == 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):
if any(payment.journal_id != valid_payments[0].journal_id for payment in valid_payments):
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:
if not valid_payments[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
SELECT payment.check_number
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
WHERE payment.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)
last_check_number = (self.env.cr.fetchone() or (False,))[0]
number_len = len(last_check_number or "")
next_check_number = f'{int(last_check_number) + 1:0{number_len}}'
return {
'name': _('Print Pre-numbered Checks'),
@ -190,23 +194,20 @@ class AccountPayment(models.Model):
'view_mode': 'form',
'target': 'new',
'context': {
'payment_ids': self.ids,
'payment_ids': valid_payments.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})
valid_payments.filtered(lambda r: r.state == 'draft').action_post()
return valid_payments.do_print_checks()
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
check_layout = self.journal_id.bank_check_printing_layout or 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.")
@ -215,7 +216,7 @@ class AccountPayment(models.Model):
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})
self.write({'is_sent': 'True'})
return report_action.report_action(self)
#######################
@ -237,14 +238,14 @@ class AccountPayment(models.Model):
'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,
'memo': self.memo,
'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.
""" 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 = []
@ -258,8 +259,9 @@ class AccountPayment(models.Model):
"""
self.ensure_one()
def prepare_vals(invoice, partials):
number = ' - '.join([invoice.name, invoice.ref] if invoice.ref else [invoice.name])
def prepare_vals(invoice, partials=None, current_amount=0):
invoice_name = invoice.name or '/'
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
@ -268,51 +270,65 @@ class AccountPayment(models.Model):
invoice_sign = -1
partial_field = 'credit_amount_currency'
if invoice.currency_id.is_zero(invoice.amount_residual):
amount_residual = invoice.amount_residual - current_amount
if invoice.currency_id.is_zero(amount_residual):
amount_residual_str = '-'
else:
amount_residual_str = formatLang(self.env, invoice_sign * invoice.amount_residual, currency_obj=invoice.currency_id)
amount_residual_str = formatLang(self.env, invoice_sign * amount_residual, currency_obj=invoice.currency_id)
amount_paid = current_amount if current_amount else sum(partials.mapped(partial_field))
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),
'amount_paid': formatLang(self.env, invoice_sign * amount_paid, 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)
if self.move_id:
# Decode the reconciliation to keep only invoices.
term_lines = self.move_id.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(include_receipts=True))
# 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']
# 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
else:
stub_lines = [prepare_vals(invoice, partials)
for invoice, partials in invoice_map.items()
if invoice.move_type in ('in_invoice', 'in_receipt')]
invoices = self.invoice_ids.filtered(lambda x: x.is_outbound(include_receipts=True))
remaining = self.amount
stub_lines = []
type_groups = {
('in_invoice', 'in_receipt'): _("Bills"),
('out_refund',): _("Refunds"),
}
invoices_grouped = invoices.grouped(lambda i: next(group for group in type_groups if i.move_type in group))
for type_group, invoices in invoices_grouped.items():
invoices = iter(invoices.sorted(lambda x: x.invoice_date_due or x.date))
if len(invoices_grouped) > 1:
stub_lines += [{'header': True, 'name': type_groups[type_group]}]
if self.move_id:
stub_lines += [
prepare_vals(invoice, partials=invoice_map[invoice])
for invoice in invoices
]
else:
while remaining and (invoice := next(invoices, None)):
current_amount = min(remaining, invoice.currency_id._convert(
from_amount=invoice.amount_residual,
to_currency=self.currency_id,
))
stub_lines += [prepare_vals(invoice, current_amount=current_amount)]
remaining -= current_amount
# Crop the stub lines or split them on multiple pages
if not self.company_id.account_check_printing_multi_stub:

View file

@ -10,5 +10,5 @@ class AccountPaymentMethod(models.Model):
@api.model
def _get_payment_method_information(self):
res = super()._get_payment_method_information()
res['check_printing'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]}
res['check_printing'] = {'mode': 'multi', 'type': ('bank',)}
return res

View file

@ -3,7 +3,7 @@
from odoo import models, fields
class res_company(models.Model):
class ResCompany(models.Model):
_inherit = "res.company"
# This field needs to be overridden with `selection_add` in the modules which intends to add report layouts.

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
property_payment_method_id = fields.Many2one(
comodel_name='account.payment.method',
string='Payment Method',
company_dependent=True,
domain="[('payment_type', '=', 'outbound')]",
help="Preferred payment method when paying this vendor. This is used to filter vendor bills"
" by preferred payment method to register payments in mass. Use cases: create bank"
" files for batch wires, check runs.",
)