Payment Term refactoring to make it work like dateutil's relativedelta.
review195001 issue2320
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user