262 lines
8.7 KiB
Python
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.",
|
|
)
|