From 57ce272f7c36bbd53457a63efeb7b100be21c743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 5 Oct 2016 19:20:00 +0200 Subject: [PATCH] Add distribution account type A new type of account is added for distribution. It is used on Account Entry to yield the lines to create for a accounting line. issue5908 review31551002 --- CHANGELOG | 2 + __init__.py | 1 + account.py | 104 ++++++++++++++++++++++++++++- account.xml | 30 +++++++++ tests/test_analytic_account.py | 21 ++++++ view/account_distribution_form.xml | 14 ++++ view/account_distribution_list.xml | 10 +++ view/account_form.xml | 3 + 8 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 view/account_distribution_form.xml create mode 100644 view/account_distribution_list.xml diff --git a/CHANGELOG b/CHANGELOG index cd4592579e..9779efb505 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Add get_analytic_lines on AnalyticAccountEntry +* Add distribution account type * Add analytic state on move line * Improve account tree creation * Remove name, journal, reference, party and active from line diff --git a/__init__.py b/__init__.py index 45c6ce437f..1c9bee1638 100644 --- a/__init__.py +++ b/__init__.py @@ -9,6 +9,7 @@ from .line import * def register(): Pool.register( Account, + AccountDistribution, OpenChartAccountStart, AnalyticAccountEntry, Line, diff --git a/account.py b/account.py index 7869d5a6a7..07ba63a2a5 100644 --- a/account.py +++ b/account.py @@ -14,7 +14,8 @@ from trytond.pyson import Eval, If, PYSONEncoder, PYSONDecoder from trytond.transaction import Transaction from trytond.pool import Pool -__all__ = ['Account', 'OpenChartAccountStart', 'OpenChartAccount', +__all__ = ['Account', 'AccountDistribution', + 'OpenChartAccountStart', 'OpenChartAccount', 'AnalyticAccountEntry', 'AnalyticMixin'] @@ -34,6 +35,7 @@ class Account(ModelSQL, ModelView): ('root', 'Root'), ('view', 'View'), ('normal', 'Normal'), + ('distribution', 'Distribution'), ], 'Type', required=True) root = fields.Many2One('analytic_account.account', 'Root', select=True, domain=[ @@ -88,12 +90,28 @@ class Account(ModelSQL, ModelView): }, depends=['type'], help="Make this account mandatory when filling documents") + distributions = fields.One2Many( + 'analytic_account.account.distribution', 'parent', + "Distributions", + states={ + 'invisible': Eval('type') != 'distribution', + 'required': Eval('type') == 'distribution', + }, + depends=['type']) + distribution_parents = fields.Many2Many( + 'analytic_account.account.distribution', 'account', 'parent', + "Distribution Parents", readonly=True) @classmethod def __setup__(cls): super(Account, cls).__setup__() cls._order.insert(0, ('code', 'ASC')) cls._order.insert(1, ('name', 'ASC')) + cls._error_messages.update({ + 'invalid_distribution': ( + 'The distribution sum of account "%(account)" ' + 'is not 100%.'), + }) @classmethod def __register__(cls, module_name): @@ -132,6 +150,17 @@ class Account(ModelSQL, ModelView): def validate(cls, accounts): super(Account, cls).validate(accounts) cls.check_recursion(accounts) + cls.check_recursion(accounts, parent='distribution_parents') + for account in accounts: + account.check_distribution() + + def check_distribution(self): + if self.type != 'distribution': + return + if sum((d.ratio for d in self.distributions)) != 1: + self.raise_user_error('invalid_distribution', { + 'account': self.rec_name, + }) @fields.depends('company') def on_change_with_currency(self, name=None): @@ -269,6 +298,30 @@ class Account(ModelSQL, ModelView): else: return [(cls._rec_name,) + tuple(clause[1:])] + def distribute(self, amount): + "Return a list of (account, amount) distribution" + assert self.type in {'normal', 'distribution'} + if self.type == 'normal': + return [(self, amount)] + else: + result = [] + remainder = amount + for distribution in self.distributions: + account = distribution.account + ratio = distribution.ratio + current_amount = self.currency.round(amount * ratio) + remainder -= current_amount + result.extend(account.distribute(current_amount)) + if remainder: + i = 0 + while remainder: + account, amount = result[i] + rounding = self.currency.rounding.copy_sign(remainder) + result[i] = (account, amount - rounding) + remainder -= rounding + i = (i + 1) % len(result) + return result + class OpenChartAccountStart(ModelView): 'Open Chart of Accounts' @@ -298,6 +351,37 @@ class OpenChartAccount(Wizard): return 'end' +class AccountDistribution(ModelView, ModelSQL): + "Analytic Account Distribution" + __name__ = 'analytic_account.account.distribution' + parent = fields.Many2One( + 'analytic_account.account', "Parent", required=True, select=True) + root = fields.Function( + fields.Many2One('analytic_account.account', "Root"), + 'on_change_with_root') + account = fields.Many2One( + 'analytic_account.account', "Account", required=True, + domain=[ + ('root', '=', Eval('root', -1)), + ], + depends=['root']) + ratio = fields.Numeric("Ratio", required=True, + domain=[ + ('ratio', '>=', 0), + ('ratio', '<=', 1), + ]) + + @classmethod + def __setup__(cls): + super(AccountDistribution, cls).__setup__() + cls._order.insert(0, ('ratio', 'DESC')) + + @fields.depends('parent', '_parent_parent.root') + def on_change_with_root(self, name=None): + if self.parent: + return self.parent.root.id + + class AnalyticAccountEntry(ModelView, ModelSQL): 'Analytic Account Entry' __name__ = 'analytic.account.entry' @@ -318,7 +402,7 @@ class AnalyticAccountEntry(ModelView, ModelSQL): }, domain=[ ('root', '=', Eval('root')), - ('type', '=', 'normal'), + ('type', 'in', ['normal', 'distribution']), ], depends=['root', 'required', 'company']) required = fields.Function(fields.Boolean('Required'), @@ -394,6 +478,22 @@ class AnalyticAccountEntry(ModelView, ModelSQL): def search_company(cls, name, clause): return [] + def get_analytic_lines(self, line, date): + "Yield analytic lines for the accounting line and the date" + pool = Pool() + AnalyticLine = pool.get('analytic_account.line') + + if not self.account: + return + amount = line.debit or line.credit + for account, amount in self.account.distribute(amount): + analytic_line = AnalyticLine() + analytic_line.debit = amount if line.debit else Decimal(0) + analytic_line.credit = amount if line.credit else Decimal(0) + analytic_line.account = account + analytic_line.date = date + yield analytic_line + class AnalyticMixin(ModelSQL): diff --git a/account.xml b/account.xml index f8442a24b1..5e854b683e 100644 --- a/account.xml +++ b/account.xml @@ -114,6 +114,36 @@ this repository contains the full copyright notices and license terms. --> + + analytic_account.account.distribution + form + account_distribution_form + + + + analytic_account.account.distribution + tree + account_distribution_list + + + + + + + + + + + + + + + + + + analytic_account.open_chart.start form diff --git a/tests/test_analytic_account.py b/tests/test_analytic_account.py index 1bd1441934..276d0e9282 100644 --- a/tests/test_analytic_account.py +++ b/tests/test_analytic_account.py @@ -12,6 +12,7 @@ from trytond.pool import Pool from trytond.modules.company.tests import create_company, set_company from trytond.modules.account.tests import create_chart, get_fiscalyear +from trytond.modules.currency.tests import create_currency class AnalyticAccountTestCase(ModuleTestCase): @@ -211,6 +212,26 @@ class AnalyticAccountTestCase(ModuleTestCase): self.assertEqual(expense_line.analytic_state, 'valid') + @with_transaction() + def test_account_distribute(self): + "Test account distribute" + pool = Pool() + Account = pool.get('analytic_account.account') + Distribution = pool.get('analytic_account.account.distribution') + + currency = create_currency('usd') + account1 = Account(type='normal', currency=currency) + account2 = Account(type='normal', currency=currency) + account = Account(type='distribution', currency=currency) + account.distributions = [ + Distribution(account=account1, ratio=Decimal('0.7')), + Distribution(account=account2, ratio=Decimal('0.3')), + ] + + self.assertListEqual( + account.distribute(Decimal('100.03')), + [(account1, Decimal('70.02')), (account2, Decimal('30.01'))]) + def suite(): suite = trytond.tests.test_tryton.suite() diff --git a/view/account_distribution_form.xml b/view/account_distribution_form.xml new file mode 100644 index 0000000000..0be6fadcbc --- /dev/null +++ b/view/account_distribution_form.xml @@ -0,0 +1,14 @@ + + +
+