Payment Term refactoring to make it work like dateutil's relativedelta.

review195001
issue2320
This commit is contained in:
Albert Cervera i Areny
2012-04-01 16:08:13 +02:00
parent bfd72d50ed
commit f720f3b5ea
5 changed files with 265 additions and 136 deletions
+3
View File
@@ -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
-2
View File
@@ -3,8 +3,6 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<menuitem name="Payment Terms" parent="account.menu_account_configuration"
id="menu_payment_terms_configuration"/>
<menuitem name="Invoices" parent="account.menu_account"
id="menu_invoices" sequence="20"/>
+166 -84
View File
@@ -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()
+32 -49
View File
@@ -3,6 +3,8 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<menuitem name="Payment Terms" parent="account.menu_account_configuration"
id="menu_payment_terms_configuration"/>
<record model="ir.ui.view" id="payment_term_view_form">
<field name="model">account.invoice.payment_term</field>
<field name="type">form</field>
@@ -83,8 +85,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="percentage"/>
<field name="amount"/>
<field name="currency"/>
<field name="months"/>
<field name="month"/>
<field name="weeks"/>
<field name="weekday"/>
<field name="days"/>
<field name="delay"/>
<field name="day"/>
<field name="sequence" tree_invisible="1"/>
<field name="currency_digits" tree_invisible="1"/>
</tree>
@@ -103,8 +109,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="percentage"/>
<field name="amount"/>
<field name="currency"/>
<field name="months"/>
<field name="month"/>
<field name="weeks"/>
<field name="weekday"/>
<field name="days"/>
<field name="delay"/>
<field name="day"/>
<field name="sequence" tree_invisible="1"/>
<field name="currency_digits" tree_invisible="1"/>
</tree>
@@ -120,21 +130,28 @@ this repository contains the full copyright notices and license terms. -->
<form string="Payment Term Lines">
<label name="type"/>
<field name="type"/>
<group col="1" id="label_amount_percent">
<label name="percentage" yfill="1" xexpand="1" yexpand="1"/>
<label name="amount" yfill="1" xexpand="1" yexpand="1"/>
</group>
<group col="3" id="amount_percent">
<field name="percentage" colspan="3"/>
<field name="amount"/>
<label name="currency"/>
<field name="currency"/>
</group>
<newline/>
<label name="amount"/>
<field name="amount"/>
<label name="currency"/>
<field name="currency"/>
<newline/>
<label name="percentage"/>
<field name="percentage"/>
<label name="divisor"/>
<field name="divisor"/>
<label name="months"/>
<field name="months"/>
<label name="month"/>
<field name="month"/>
<label name="weeks"/>
<field name="weeks"/>
<label name="weekday"/>
<field name="weekday"/>
<label name="days"/>
<field name="days"/>
<label name="delay"/>
<field name="delay"/>
<field name="currency_digits" invisible="1"/>
<label name="day"/>
<field name="day"/>
</form>
]]>
</field>
@@ -157,39 +174,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_delete" eval="True"/>
</record>
<record model="account.invoice.payment_term.line.type"
id="payment_term_line_type_fixed">
<field name="name">Fixed</field>
<field name="code">fixed</field>
</record>
<record model="account.invoice.payment_term.line.type"
id="payment_term_line_type_percent">
<field name="name">Percentage</field>
<field name="code">percent</field>
</record>
<record model="account.invoice.payment_term.line.type"
id="payment_term_line_type_remainder">
<field name="name">Remainder</field>
<field name="code">remainder</field>
</record>
<record model="account.invoice.payment_term.line.delay"
id="payment_term_line_delay_net_days">
<field name="name">Net Days</field>
<field name="code">net_days</field>
</record>
<record model="account.invoice.payment_term.line.delay"
id="payment_term_line_delay_end_month">
<field name="name">End Month</field>
<field name="code">end_month</field>
</record>
<record model="ir.model.access" id="access_payment_term_line_type">
<field name="model"
search="[('model', '=', 'account.invoice.payment_term.line.type')]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
</data>
</tryton>
+64 -1
View File
@@ -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(