diff --git a/CHANGELOG b/CHANGELOG index 9344bb1b15..c0ab006909 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Add divisor field to Payment Term Lines +* Allow setting months, weeks, days as well as month, weekday and day in + Payment Term Lines * Rename field payment_term to customer_payment_term Version 2.2.0 - 2011-10-24 diff --git a/invoice.xml b/invoice.xml index 16814000f4..e53f452ced 100644 --- a/invoice.xml +++ b/invoice.xml @@ -3,8 +3,6 @@ this repository contains the full copyright notices and license terms. --> - diff --git a/payment_term.py b/payment_term.py index 81f4534561..bbf6b42fec 100644 --- a/payment_term.py +++ b/payment_term.py @@ -31,32 +31,40 @@ class PaymentTerm(ModelSQL, ModelView): return True def compute(self, amount, currency, payment_term, date=None): - ''' - Return list with (date, amount) for each payment term lines - ''' + """Calculate payment terms and return a list of tuples + with (date, amount) for each payment term line. + + amount must be a Decimal used for the calculation and + both currency and payment_term must be BrowseRecord. If + specified, date will be used as the start date, otherwise + current date will be used. + """ #TODO implement business_days # http://pypi.python.org/pypi/BusinessHours/ pool = Pool() - type_obj = pool.get('account.invoice.payment_term.line.type') - delay_obj = pool.get( - 'account.invoice.payment_term.line.delay') + line_obj = pool.get('account.invoice.payment_term.line') currency_obj = pool.get('currency.currency') date_obj = pool.get('ir.date') + sign = 1 if amount >= Decimal('0.0') else -1 res = [] if date is None: date = date_obj.today() remainder = amount for line in payment_term.lines: - value = type_obj.get_value(line, remainder, currency) - value_date = delay_obj.get_date(line, date) + value = line_obj.get_value(line, remainder, amount, currency) + value_date = line_obj.get_date(line, date) if not value or not value_date: if (not remainder) and line.amount: self.raise_user_error('invalid_line') else: continue + if ((remainder - value) * sign) < Decimal('0.0'): + res.append((value_date, remainder)) + break res.append((value_date, value)) remainder -= value + if not currency_obj.is_zero(currency, remainder): self.raise_user_error('missing_remainder') return res @@ -64,59 +72,6 @@ class PaymentTerm(ModelSQL, ModelView): PaymentTerm() -class PaymentTermLineType(ModelSQL, ModelView): - 'Payment Term Line Type' - _name = 'account.invoice.payment_term.line.type' - _description = __doc__ - name = fields.Char('Name', size=None, translate=True, required=True) - code = fields.Char('Code', size=None, required=True) - - def __init__(self): - super(PaymentTermLineType, self).__init__() - self._sql_constraints += [ - ('code_uniq', 'UNIQUE(code)', 'The code must be unique!'), - ] - self._order.insert(0, ('name', 'ASC')) - - def get_value(self, line, amount, currency): - currency_obj = Pool().get('currency.currency') - if line.type == 'fixed': - return currency_obj.compute(line.currency, line.amount, currency) - elif line.type == 'percent': - return currency_obj.round(currency, - amount * line.percentage / Decimal('100')) - elif line.type == 'remainder': - return currency_obj.round(currency, amount) - return None - -PaymentTermLineType() - - -class PaymentTermLineDelay(ModelSQL, ModelView): - 'Payment Term Line Delay' - _name = 'account.invoice.payment_term.line.delay' - _description = __doc__ - name = fields.Char('Name', size=None, translate=True, required=True) - code = fields.Char('Code', size=None, required=True) - - def __init__(self): - super(PaymentTermLineDelay, self).__init__() - self._sql_constraints += [ - ('code_uniq', 'UNIQUE(code)', 'The code must be unique!'), - ] - self._order.insert(0, ('name', 'ASC')) - - def get_date(self, line, date): - value = None - if line.delay in ('net_days', 'end_month'): - value = date + relativedelta(days=line.days) - if line.delay == 'end_month': - value += relativedelta(day=31) - return value - -PaymentTermLineDelay() - - class PaymentTermLine(ModelSQL, ModelView): 'Payment Term Line' _name = 'account.invoice.payment_term.line' @@ -125,13 +80,23 @@ class PaymentTermLine(ModelSQL, ModelView): help='Use to order lines in ascending order') payment = fields.Many2One('account.invoice.payment_term', 'Payment Term', required=True, ondelete="CASCADE") - type = fields.Selection('get_type', 'Type', required=True, + type = fields.Selection([ + ('fixed', 'Fixed'), + ('percent', 'Percentage on Remainder'), + ('percent_on_total', 'Percentage on Total'), + ('remainder', 'Remainder'), + ], 'Type', required=True, on_change=['type']) percentage = fields.Numeric('Percentage', digits=(16, 8), states={ - 'invisible': Eval('type') != 'percent', - 'required': Eval('type') == 'percent', - }, depends=['type'], help='In %') + 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), + 'required': Eval('type').in_(['percent', 'percent_on_total']), + }, on_change=['percentage'], depends=['type']) + divisor = fields.Numeric('Divisor', digits=(16, 8), + states={ + 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), + 'required': Eval('type').in_(['percent', 'percent_on_total']), + }, on_change=['divisor'], depends=['type']) amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('type') != 'fixed', @@ -144,12 +109,50 @@ class PaymentTermLine(ModelSQL, ModelView): }, depends=['type']) currency_digits = fields.Function(fields.Integer('Currency Digits', on_change_with=['currency']), 'get_currency_digits') + day = fields.Integer('Day of Month') + month = fields.Selection([ + (None, ''), + ('1', 'January'), + ('2', 'February'), + ('3', 'March'), + ('4', 'April'), + ('5', 'May'), + ('6', 'June'), + ('7', 'July'), + ('8', 'August'), + ('9', 'September'), + ('10', 'October'), + ('11', 'November'), + ('12','December'), + ], 'Month', sort=False) + weekday = fields.Selection([ + (None, ''), + ('0', 'Monday'), + ('1', 'Tuesday'), + ('2', 'Wednesday'), + ('3', 'Thursday'), + ('4', 'Friday'), + ('5', 'Saturday'), + ('6', 'Sunday'), + ], 'Day of Week', sort=False) + months = fields.Integer('Number of Months', required=True) + weeks = fields.Integer('Number of Weeks', required=True) days = fields.Integer('Number of Days', required=True) - delay = fields.Selection('get_delay', 'Condition', required=True) def __init__(self): super(PaymentTermLine, self).__init__() self._order.insert(0, ('sequence', 'ASC')) + self._sql_constraints += [ + ('day', 'CHECK(day BETWEEN 1 AND 31)', + 'Day of month must be between 1 and 31.'), + ] + self._constraints += [ + ('check_percentage_and_divisor', 'invalid_percentage_and_divisor'), + ] + self._error_messages.update({ + 'invalid_percentage_and_divisor': 'Percentage and ' + 'Divisor values are not consistent.', + }) def init(self, module_name): super(PaymentTermLine, self).init(module_name) @@ -162,26 +165,33 @@ class PaymentTermLine(ModelSQL, ModelView): 'SET percentage = percent * 100') table.drop_column('percent', exception=True) - def default_type(self): - return 'remainder' - - def default_delay(self): - return 'net_days' + # Migration from 2.2 + if table.column_exist('delay'): + cursor.execute('UPDATE "' + self._table + '" SET day = 31 ' + "WHERE delay = 'end_month'") + table.drop_column('delay', exception=True) + ids = self.search([]) + for line in self.browse(ids): + if line.percentage: + self.write(line.id, { + 'divisor': self.round(Decimal('100.0') / + line.percentage, self.divisor.digits[1]), + }) def default_currency_digits(self): return 2 - def get_type(self): - type_obj = Pool().get('account.invoice.payment_term.line.type') - type_ids = type_obj.search([]) - types = type_obj.browse(type_ids) - return [(x.code, x.name) for x in types] + def default_type(self): + return 'remainder' - def get_delay(self): - delay_obj = Pool().get('account.invoice.payment_term.line.delay') - delay_ids = delay_obj.search([]) - delays = delay_obj.browse(delay_ids) - return [(x.code, x.name) for x in delays] + def default_months(self): + return 0 + + def default_weeks(self): + return 0 + + def default_days(self): + return 0 def on_change_type(self, vals): if not 'type' in vals: @@ -189,11 +199,28 @@ class PaymentTermLine(ModelSQL, ModelView): res = {} if vals['type'] != 'fixed': res['amount'] = Decimal('0.0') - res['currency'] = None - if vals['type'] != 'percent': - res['percentage'] = Decimal('0.0') + res['currency'] = None + if vals['type'] not in ('percent', 'percent_on_total'): + res['percentage'] = Decimal('0.0') + res['divisor'] = Decimal('0.0') return res + def on_change_percentage(self, value): + if not value.get('percentage'): + return {'divisor': 0.0} + return { + 'divisor': self.round(Decimal('100.0') / value['percentage'], + self.divisor.digits[1]), + } + + def on_change_divisor(self, value): + if not value.get('divisor'): + return {'percentage': 0.0} + return { + 'percentage': self.round(Decimal('100.0') / value['divisor'], + self.percentage.digits[1]), + } + def on_change_with_currency_digits(self, vals): currency_obj = Pool().get('currency.currency') if vals.get('currency'): @@ -210,6 +237,61 @@ class PaymentTermLine(ModelSQL, ModelView): res[line.id] = 2 return res + def get_delta(self, line): + return { + 'day': line.day, + 'month': int(line.month) if line.month else None, + 'days': line.days, + 'weeks': line.weeks, + 'months': line.months, + 'weekday': int(line.weekday) if line.weekday else None, + } + + def get_date(self, line, date): + return date + relativedelta(**self.get_delta(line)) + + def get_value(self, line, remainder, amount, currency): + currency_obj = Pool().get('currency.currency') + if line.type == 'fixed': + return currency_obj.compute(line.currency, line.amount, currency) + elif line.type == 'percent': + return currency_obj.round(currency, + remainder * line.percentage / Decimal('100')) + elif line.type == 'percent_on_total': + return currency_obj.round(currency, + amount * line.percentage / Decimal('100')) + elif line.type == 'remainder': + return currency_obj.round(currency, remainder) + return None + + @staticmethod + def round(number, digits): + quantize = Decimal(10) ** -Decimal(digits) + return Decimal(number).quantize(quantize) + + def check_percentage_and_divisor(self, ids): + "Check consistency between percentage and divisor" + percentage_digits = self.percentage.digits[1] + divisor_digits = self.divisor.digits[1] + for line in self.browse(ids): + if line.type not in ('percent', 'percent_on_total'): + continue + if line.percentage is None or line.divisor is None: + return False + if line.percentage == line.divisor == Decimal('0.0'): + continue + percentage = line.percentage + divisor = line.divisor + calc_percentage = self.round(Decimal('100.0') / divisor, + percentage_digits) + calc_divisor = self.round(Decimal('100.0') / percentage, + divisor_digits) + if (percentage == Decimal('0.0') or divisor == Decimal('0.0') + or percentage != calc_percentage + and divisor != calc_divisor): + return False + return True + PaymentTermLine() diff --git a/payment_term.xml b/payment_term.xml index 032d9e3f7f..5a6a24a804 100644 --- a/payment_term.xml +++ b/payment_term.xml @@ -3,6 +3,8 @@ this repository contains the full copyright notices and license terms. --> + account.invoice.payment_term form @@ -83,8 +85,12 @@ this repository contains the full copyright notices and license terms. --> + + + + - + @@ -103,8 +109,12 @@ this repository contains the full copyright notices and license terms. --> + + + + - + @@ -120,21 +130,28 @@ this repository contains the full copyright notices and license terms. -->
- - Fixed - fixed - - - Percentage - percent - - - Remainder - remainder - - - - Net Days - net_days - - - End Month - end_month - - - - - - - -
diff --git a/tests/test_account_invoice.py b/tests/test_account_invoice.py index 8bce50f9f8..817d06d5a7 100644 --- a/tests/test_account_invoice.py +++ b/tests/test_account_invoice.py @@ -9,8 +9,12 @@ if os.path.isdir(DIR): sys.path.insert(0, os.path.dirname(DIR)) import unittest +import datetime +from decimal import Decimal import trytond.tests.test_tryton -from trytond.tests.test_tryton import test_view, test_depends +from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, test_view,\ + test_depends +from trytond.transaction import Transaction class AccountInvoiceTestCase(unittest.TestCase): @@ -20,6 +24,8 @@ class AccountInvoiceTestCase(unittest.TestCase): def setUp(self): trytond.tests.test_tryton.install_module('account_invoice') + self.payment_term = POOL.get('account.invoice.payment_term') + self.currency = POOL.get('currency.currency') def test0005views(self): ''' @@ -33,6 +39,63 @@ class AccountInvoiceTestCase(unittest.TestCase): ''' test_depends() + def test0010payment_term(self): + ''' + Test payment_term. + ''' + with Transaction().start(DB_NAME, USER, CONTEXT) as transaction: + cu1_id = self.currency.create({ + 'name': 'cu1', + 'symbol': 'cu1', + 'code': 'cu1' + }) + cu1 = self.currency.browse(cu1_id) + + term_id = self.payment_term.create({ + 'name': '30 days, 1 month, 1 month + 15 days', + 'lines': [ + ('create', { + 'sequence': 0, + 'type': 'percent', + 'divisor': 4, + 'percentage': 25, + 'days': 30, + }), + ('create', { + 'sequence': 1, + 'type': 'percent_on_total', + 'divisor': 4, + 'percentage': 25, + 'months': 1, + }), + ('create', { + 'sequence': 2, + 'type': 'fixed', + 'months': 1, + 'days': 30, + 'amount': Decimal('396.84'), + 'currency': cu1_id, + }), + ('create', { + 'sequence': 3, + 'type': 'remainder', + 'months': 2, + 'days': 30, + 'day': 15, + })] + }) + term = self.payment_term.browse(term_id) + terms = term.compute(Decimal('1587.35'), cu1, term, + date=datetime.date(2011,10,1)) + self.assertEqual(terms, [ + (datetime.date(2011,10,31), Decimal('396.84')), + (datetime.date(2011,11,01), Decimal('396.84')), + (datetime.date(2011,12,01), Decimal('396.84')), + (datetime.date(2012,01,14), Decimal('396.83')), + ]) + + + def suite(): suite = trytond.tests.test_tryton.suite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase(