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 @@
+
+
+
diff --git a/view/account_distribution_list.xml b/view/account_distribution_list.xml
new file mode 100644
index 0000000000..94eaf0e142
--- /dev/null
+++ b/view/account_distribution_list.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/account_form.xml b/view/account_form.xml
index 03b81c9c08..0fc98a6555 100644
--- a/view/account_form.xml
+++ b/view/account_form.xml
@@ -27,6 +27,9 @@ this repository contains the full copyright notices and license terms. -->
+
+
+