Files
Odoo-Modules/Fusion Accounting/models/check_printing.py
2026-02-22 01:22:18 -05:00

262 lines
8.7 KiB
Python

"""
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.",
)