Initial commit
This commit is contained in:
261
Fusion Accounting/models/check_printing.py
Normal file
261
Fusion Accounting/models/check_printing.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
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.",
|
||||
)
|
||||
Reference in New Issue
Block a user