""" Fusion Accounting - Check Printing Support Extends ``account.payment`` with fields and logic required for printing physical checks, including automatic check numbering and amount-to-words conversion. """ import logging import math from odoo import api, fields, models, _ from odoo.exceptions import UserError _log = logging.getLogger(__name__) # ====================================================================== # Amount-to-words conversion (English) # ====================================================================== _ONES = [ '', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen', ] _TENS = [ '', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety', ] _SCALES = [ (10 ** 12, 'Trillion'), (10 ** 9, 'Billion'), (10 ** 6, 'Million'), (10 ** 3, 'Thousand'), (10 ** 2, 'Hundred'), ] def _int_to_words(number): """Convert a non-negative integer to its English word representation. :param int number: A non-negative integer (0 .. 999 999 999 999 999). :return: English words, e.g. ``'One Thousand Two Hundred Thirty-Four'``. :rtype: str """ if number == 0: return 'Zero' if number < 0: return 'Minus ' + _int_to_words(-number) parts = [] for scale_value, scale_name in _SCALES: count, number = divmod(number, scale_value) if count: if scale_value == 100: parts.append(f'{_int_to_words(count)} {scale_name}') else: parts.append(f'{_int_to_words(count)} {scale_name}') if 0 < number < 20: parts.append(_ONES[number]) elif number >= 20: tens_idx, ones_idx = divmod(number, 10) word = _TENS[tens_idx] if ones_idx: word += '-' + _ONES[ones_idx] parts.append(word) return ' '.join(parts) def amount_to_words(amount, currency_name='Dollars', cents_name='Cents'): """Convert a monetary amount to an English sentence. Example:: >>> amount_to_words(1234.56) 'One Thousand Two Hundred Thirty-Four Dollars and Fifty-Six Cents' :param float amount: The monetary amount. :param str currency_name: Name of the major currency unit. :param str cents_name: Name of the minor currency unit. :return: The amount expressed in English words. :rtype: str """ if amount < 0: return 'Minus ' + amount_to_words(-amount, currency_name, cents_name) whole = int(amount) # Round to avoid floating-point artefacts (e.g. 1.005 -> 0 cents) cents = round((amount - whole) * 100) if cents >= 100: whole += 1 cents = 0 result = f'{_int_to_words(whole)} {currency_name}' if cents: result += f' and {_int_to_words(cents)} {cents_name}' else: result += f' and Zero {cents_name}' return result # ====================================================================== # Odoo model # ====================================================================== class FusionCheckPrinting(models.Model): """Adds check-printing capabilities to ``account.payment``. Features -------- * Manual or automatic check numbering per journal. * Human-readable amount-in-words field for check printing. * Validation to prevent duplicate check numbers within a journal. """ _inherit = 'account.payment' # ------------------------------------------------------------------ # Fields # ------------------------------------------------------------------ check_number = fields.Char( string='Check Number', copy=False, tracking=True, help="The number printed on the physical check.", ) check_manual_sequencing = fields.Boolean( string='Manual Numbering', related='journal_id.fusion_check_manual_sequencing', readonly=True, help="When enabled, check numbers are entered manually instead " "of being assigned automatically.", ) check_next_number = fields.Char( string='Next Check Number', related='journal_id.fusion_check_next_number', readonly=False, help="The next check number to be assigned automatically.", ) check_amount_in_words = fields.Char( string='Amount in Words', compute='_compute_check_amount_in_words', store=True, help="Human-readable representation of the payment amount, " "suitable for printing on a check.", ) # ------------------------------------------------------------------ # Computed fields # ------------------------------------------------------------------ @api.depends('amount', 'currency_id') def _compute_check_amount_in_words(self): """Compute the textual representation of the payment amount.""" for payment in self: if payment.currency_id and payment.amount: currency_name = payment.currency_id.currency_unit_label or 'Units' cents_name = payment.currency_id.currency_subunit_label or 'Cents' payment.check_amount_in_words = amount_to_words( payment.amount, currency_name=currency_name, cents_name=cents_name, ) else: payment.check_amount_in_words = '' # ------------------------------------------------------------------ # Constraints # ------------------------------------------------------------------ _sql_constraints = [ ( 'check_number_unique', 'UNIQUE(check_number, journal_id)', 'A check number must be unique per journal.', ), ] # ------------------------------------------------------------------ # Business logic # ------------------------------------------------------------------ def action_assign_check_number(self): """Assign the next available check number from the journal. If the journal is configured for manual sequencing the user must enter the number themselves; this method handles only the automatic case. :raises UserError: if the journal uses manual sequencing. """ for payment in self: if payment.check_manual_sequencing: raise UserError(_( "Journal '%(journal)s' uses manual check numbering. " "Please enter the check number manually.", journal=payment.journal_id.display_name, )) if payment.check_number: continue # already assigned next_number = payment.journal_id.fusion_check_next_number or '1' payment.check_number = next_number.zfill(6) # Increment the journal's next-number counter try: payment.journal_id.fusion_check_next_number = str( int(next_number) + 1 ) except ValueError: _log.warning( "Could not auto-increment check number '%s' on " "journal %s", next_number, payment.journal_id.display_name, ) def action_print_check(self): """Print the check report for the selected payments. Automatically assigns check numbers to any payment that does not already have one. :return: Report action dictionary. :rtype: dict """ payments_without_number = self.filtered( lambda p: not p.check_number and not p.check_manual_sequencing ) payments_without_number.action_assign_check_number() missing = self.filtered(lambda p: not p.check_number) if missing: raise UserError(_( "The following payments still have no check number:\n" "%(payments)s\nPlease assign check numbers before printing.", payments=', '.join(missing.mapped('name')), )) return self.env.ref( 'fusion_accounting.action_report_check' ).report_action(self) class FusionAccountJournalCheck(models.Model): """Adds check-numbering configuration to ``account.journal``.""" _inherit = 'account.journal' fusion_check_manual_sequencing = fields.Boolean( string='Manual Check Numbering', help="Enable to enter check numbers manually instead of using " "automatic sequencing.", ) fusion_check_next_number = fields.Char( string='Next Check Number', default='1', help="The next check number that will be automatically assigned " "when printing checks from this journal.", )