diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account_check_printing/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/account_check_printing/models')
7 files changed, 601 insertions, 0 deletions
diff --git a/addons/account_check_printing/models/__init__.py b/addons/account_check_printing/models/__init__.py new file mode 100644 index 00000000..7341412a --- /dev/null +++ b/addons/account_check_printing/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# 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 res_company +from . import res_config_settings +from . import res_partner diff --git a/addons/account_check_printing/models/account_journal.py b/addons/account_check_printing/models/account_journal.py new file mode 100644 index 00000000..004293a1 --- /dev/null +++ b/addons/account_check_printing/models/account_journal.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +class AccountJournal(models.Model): + _inherit = "account.journal" + + check_manual_sequencing = fields.Boolean( + string='Manual Numbering', + default=False, + help="Check this option if your pre-printed checks are not numbered.", + ) + check_sequence_id = fields.Many2one( + comodel_name='ir.sequence', + string='Check Sequence', + readonly=True, + copy=False, + help="Checks numbering sequence.", + ) + check_next_number = fields.Char( + string='Next Check Number', + compute='_compute_check_next_number', + inverse='_inverse_check_next_number', + help="Sequence number of the next printed check.", + ) + check_printing_payment_method_selected = fields.Boolean( + compute='_compute_check_printing_payment_method_selected', + help="Technical feature used to know whether check printing was enabled as payment method.", + ) + + @api.depends('check_manual_sequencing') + def _compute_check_next_number(self): + for journal in self: + sequence = journal.check_sequence_id + if sequence: + journal.check_next_number = sequence.get_next_char(sequence.number_next_actual) + else: + journal.check_next_number = 1 + + def _inverse_check_next_number(self): + for journal in self: + 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: + 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) + journal.check_sequence_id.sudo().padding = len(journal.check_next_number) + + @api.depends('type') + def _compute_outbound_payment_method_ids(self): + super()._compute_outbound_payment_method_ids() + for journal in self: + if journal.type == 'cash': + check_method = self.env.ref('account_check_printing.account_payment_method_check') + journal.outbound_payment_method_ids -= check_method + + @api.depends('outbound_payment_method_ids') + def _compute_check_printing_payment_method_selected(self): + for journal in self: + journal.check_printing_payment_method_selected = any( + pm.code == 'check_printing' + for pm in journal.outbound_payment_method_ids + ) + + @api.model + def create(self, vals): + rec = super(AccountJournal, self).create(vals) + if not rec.check_sequence_id: + rec._create_check_sequence() + return rec + + @api.returns('self', lambda value: value.id) + def copy(self, default=None): + rec = super(AccountJournal, self).copy(default) + rec._create_check_sequence() + return rec + + def _create_check_sequence(self): + """ 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"), + 'implementation': 'no_gap', + 'padding': 5, + 'number_increment': 1, + 'company_id': journal.company_id.id, + }) + + def _default_outbound_payment_methods(self): + methods = super(AccountJournal, self)._default_outbound_payment_methods() + return methods + self.env.ref('account_check_printing.account_payment_method_check') + + @api.model + def _enable_check_printing_on_bank_journals(self): + """ Enables check printing payment method and add a check sequence on bank journals. + Called upon module installation via data file. + """ + check_method = self.env.ref('account_check_printing.account_payment_method_check') + for bank_journal in self.search([('type', '=', 'bank')]): + bank_journal._create_check_sequence() + bank_journal.outbound_payment_method_ids += check_method + + def get_journal_dashboard_datas(self): + domain_checks_to_print = [ + ('journal_id', '=', self.id), + ('payment_method_id.code', '=', 'check_printing'), + ('state', '=', 'posted'), + ('is_move_sent','=', False), + ] + return dict( + super(AccountJournal, self).get_journal_dashboard_datas(), + num_checks_to_print=self.env['account.payment'].search_count(domain_checks_to_print), + ) + + def action_checks_to_print(self): + check_method = self.env.ref('account_check_printing.account_payment_method_check') + return { + 'name': _('Checks to Print'), + 'type': 'ir.actions.act_window', + 'view_mode': 'list,form,graph', + 'res_model': 'account.payment', + 'context': dict( + self.env.context, + search_default_checks_to_send=1, + journal_id=self.id, + default_journal_id=self.id, + default_payment_type='outbound', + default_payment_method_id=check_method.id, + ), + } diff --git a/addons/account_check_printing/models/account_move.py b/addons/account_check_printing/models/account_move.py new file mode 100644 index 00000000..c5c8dcb6 --- /dev/null +++ b/addons/account_check_printing/models/account_move.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +from odoo import models, fields, api + + +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, + ) + + @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 diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py new file mode 100644 index 00000000..f8c3a9c4 --- /dev/null +++ b/addons/account_check_printing/models/account_payment.py @@ -0,0 +1,321 @@ +# -*- 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_id(self): + super()._compute_payment_method_id() + for record in self: + preferred = record.partner_id.with_company(record.company_id).property_payment_method_id + if record.payment_type == 'outbound' and preferred in record.journal_id.outbound_payment_method_ids: + record.payment_method_id = preferred + +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.", + ) + + @api.constrains('check_number', 'journal_id') + def _constrains_check_number(self): + if not self: + return + try: + self.mapped(lambda p: str(int(p.check_number))) + except ValueError: + raise ValidationError(_('Check numbers can only consist of digits')) + self.flush() + 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::INTEGER = other_payment.check_number::INTEGER + 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' + """, { + 'ids': tuple(self.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_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_id(self): + super()._compute_payment_method_id() + for record in self: + preferred = record.partner_id.with_company(record.company_id).property_payment_method_id + if record.payment_type == 'outbound' and preferred in record.journal_id.outbound_payment_method_ids: + record.payment_method_id = preferred + + def action_post(self): + res = super(AccountPayment, self).action_post() + 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 res + + 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_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 check_number IS NOT NULL + ORDER BY check_number::INTEGER 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(): + 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.internal_type in ('receivable', '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()) + 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_invoice'] + + # 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 + + def _check_make_stub_line(self, invoice): + """ Return the dict used to display an invoice/refund in the stub + """ + # DEPRECATED: TO BE REMOVED IN MASTER + # Find the account.partial.reconcile which are common to the invoice and the payment + if invoice.move_type in ['in_invoice', 'out_refund']: + invoice_sign = 1 + invoice_payment_reconcile = invoice.line_ids.mapped('matched_debit_ids').filtered(lambda r: r.debit_move_id in self.line_ids) + else: + invoice_sign = -1 + invoice_payment_reconcile = invoice.line_ids.mapped('matched_credit_ids').filtered(lambda r: r.credit_move_id in self.line_ids) + + if self.currency_id != self.journal_id.company_id.currency_id: + amount_paid = abs(sum(invoice_payment_reconcile.mapped('amount_currency'))) + else: + amount_paid = abs(sum(invoice_payment_reconcile.mapped('amount'))) + + amount_residual = invoice_sign * invoice.amount_residual + + return { + 'due_date': format_date(self.env, invoice.invoice_date_due), + 'number': invoice.ref and invoice.name + ' - ' + invoice.ref or invoice.name, + 'amount_total': formatLang(self.env, invoice_sign * invoice.amount_total, currency_obj=invoice.currency_id), + 'amount_residual': formatLang(self.env, amount_residual, currency_obj=invoice.currency_id) if amount_residual * 10**4 != 0 else '-', + 'amount_paid': formatLang(self.env, invoice_sign * amount_paid, currency_obj=self.currency_id), + 'currency': invoice.currency_id, + } diff --git a/addons/account_check_printing/models/res_company.py b/addons/account_check_printing/models/res_company.py new file mode 100644 index 00000000..a96ad9a5 --- /dev/null +++ b/addons/account_check_printing/models/res_company.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields + + +class res_company(models.Model): + _inherit = "res.company" + + # This field needs to be overridden with `selection_add` in the modules which intends to add report layouts. + # The xmlID of all the report actions which are actually Check Layouts has to be kept as key of the selection. + account_check_printing_layout = fields.Selection( + string="Check Layout", + selection=[ + ('disabled', 'None'), + ], + default='disabled', + help="Select the format corresponding to the check paper you will be printing your checks on.\n" + "In order to disable the printing feature, select 'None'.", + ) + account_check_printing_date_label = fields.Boolean( + string='Print Date Label', + default=True, + help="This option allows you to print the date label on the check as per CPA.\n" + "Disable this if your pre-printed check includes the date label.", + ) + account_check_printing_multi_stub = fields.Boolean( + string='Multi-Pages Check Stub', + help="This option allows you to print check details (stub) on multiple pages if they don't fit on a single page.", + ) + account_check_printing_margin_top = fields.Float( + string='Check Top Margin', + default=0.25, + help="Adjust the margins of generated checks to make it fit your printer's settings.", + ) + account_check_printing_margin_left = fields.Float( + string='Check Left Margin', + default=0.25, + help="Adjust the margins of generated checks to make it fit your printer's settings.", + ) + account_check_printing_margin_right = fields.Float( + string='Right Margin', + default=0.25, + help="Adjust the margins of generated checks to make it fit your printer's settings.", + ) diff --git a/addons/account_check_printing/models/res_config_settings.py b/addons/account_check_printing/models/res_config_settings.py new file mode 100644 index 00000000..5716e128 --- /dev/null +++ b/addons/account_check_printing/models/res_config_settings.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + account_check_printing_layout = fields.Selection( + related='company_id.account_check_printing_layout', + string="Check Layout", + readonly=False, + help="Select the format corresponding to the check paper you will be printing your checks on.\n" + "In order to disable the printing feature, select 'None'." + ) + account_check_printing_date_label = fields.Boolean( + related='company_id.account_check_printing_date_label', + string="Print Date Label", + readonly=False, + help="This option allows you to print the date label on the check as per CPA.\n" + "Disable this if your pre-printed check includes the date label." + ) + account_check_printing_multi_stub = fields.Boolean( + related='company_id.account_check_printing_multi_stub', + string='Multi-Pages Check Stub', + readonly=False, + help="This option allows you to print check details (stub) on multiple pages if they don't fit on a single page." + ) + account_check_printing_margin_top = fields.Float( + related='company_id.account_check_printing_margin_top', + string='Check Top Margin', + readonly=False, + help="Adjust the margins of generated checks to make it fit your printer's settings." + ) + account_check_printing_margin_left = fields.Float( + related='company_id.account_check_printing_margin_left', + string='Check Left Margin', + readonly=False, + help="Adjust the margins of generated checks to make it fit your printer's settings." + ) + account_check_printing_margin_right = fields.Float( + related='company_id.account_check_printing_margin_right', + string='Check Right Margin', + readonly=False, + help="Adjust the margins of generated checks to make it fit your printer's settings." + ) diff --git a/addons/account_check_printing/models/res_partner.py b/addons/account_check_printing/models/res_partner.py new file mode 100644 index 00000000..93aca4be --- /dev/null +++ b/addons/account_check_printing/models/res_partner.py @@ -0,0 +1,19 @@ +# -*- 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.", + ) |
