Files
tryton/modules/analytic_account/account.py
T

483 lines
17 KiB
Python
Raw Normal View History

2014-12-02 12:54:55 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2016-09-27 20:55:23 +02:00
from collections import defaultdict
2021-12-11 17:58:42 +01:00
from decimal import Decimal
2016-09-27 20:55:23 +02:00
2023-01-19 11:25:25 +01:00
from sql import Column, Literal
2013-09-06 22:33:59 +02:00
from sql.aggregate import Sum
from sql.conditionals import Coalesce
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2021-12-11 17:58:42 +01:00
from trytond.model import (
2023-05-11 16:49:58 +02:00
DeactivableMixin, Index, ModelSQL, ModelView, Unique, fields, sum_tree,
tree)
from trytond.model.exceptions import AccessError
2021-08-21 09:09:55 +02:00
from trytond.modules.currency.fields import Monetary
2021-12-11 17:58:42 +01:00
from trytond.pool import Pool
from trytond.pyson import Eval, If, PYSONDecoder, PYSONEncoder
from trytond.tools import grouped_slice, is_full_text, lstrip_wildcard
2021-12-11 17:58:42 +01:00
from trytond.transaction import Transaction
2021-08-21 09:09:55 +02:00
2019-02-18 18:39:29 +01:00
from .exceptions import AccountValidationError
2018-12-29 14:20:29 +01:00
2008-05-23 15:04:38 +02:00
2018-05-12 00:47:44 +02:00
class Account(
DeactivableMixin, tree('distribution_parents'), tree(),
ModelSQL, ModelView):
2008-05-23 15:04:38 +02:00
'Analytic Account'
2012-09-24 22:11:39 +02:00
__name__ = 'analytic_account.account'
name = fields.Char("Name", required=True, translate=True)
code = fields.Char("Code")
2016-09-27 19:36:00 +02:00
company = fields.Many2One('company.company', 'Company', required=True)
2016-09-27 20:55:23 +02:00
currency = fields.Function(
fields.Many2One('currency.currency', 'Currency'),
'on_change_with_currency')
2008-05-23 15:04:38 +02:00
type = fields.Selection([
2008-05-26 11:46:01 +02:00
('root', 'Root'),
2008-05-23 15:04:38 +02:00
('view', 'View'),
('normal', 'Normal'),
2016-10-05 19:20:00 +02:00
('distribution', 'Distribution'),
2008-05-23 15:04:38 +02:00
], 'Type', required=True)
root = fields.Many2One(
'analytic_account.account', "Root",
domain=[
2016-09-27 19:36:00 +02:00
('company', '=', Eval('company', -1)),
('parent', '=', None),
('type', '=', 'root'),
],
2011-08-04 16:57:21 +02:00
states={
2011-08-21 11:20:35 +02:00
'invisible': Eval('type') == 'root',
'required': Eval('type') != 'root',
})
parent = fields.Many2One(
'analytic_account.account', "Parent",
2016-09-27 19:36:00 +02:00
domain=['OR',
('root', '=', Eval('root', -1)),
('parent', '=', None),
],
2011-08-04 16:57:21 +02:00
states={
2011-08-21 11:20:35 +02:00
'invisible': Eval('type') == 'root',
'required': Eval('type') != 'root',
})
2016-09-27 19:36:00 +02:00
childs = fields.One2Many('analytic_account.account', 'parent', 'Children',
2016-09-28 00:13:29 +02:00
states={
'invisible': Eval('id', -1) < 0,
},
2016-09-27 19:36:00 +02:00
domain=[
('company', '=', Eval('company', -1)),
])
2021-08-21 09:09:55 +02:00
balance = fields.Function(Monetary(
"Balance", currency='currency', digits='currency'),
'get_balance')
2021-08-21 09:09:55 +02:00
credit = fields.Function(Monetary(
"Credit", currency='currency', digits='currency'),
'get_credit_debit')
2021-08-21 09:09:55 +02:00
debit = fields.Function(Monetary(
"Debit", currency='currency', digits='currency'),
'get_credit_debit')
2008-05-23 15:04:38 +02:00
state = fields.Selection([
('draft', 'Draft'),
('opened', 'Opened'),
('closed', 'Closed'),
2022-02-15 00:08:47 +01:00
], "State", required=True, sort=False)
2008-05-23 15:04:38 +02:00
note = fields.Text('Note')
2016-10-05 19:20:00 +02:00
distributions = fields.One2Many(
'analytic_account.account.distribution', 'parent',
"Distributions",
states={
'invisible': Eval('type') != 'distribution',
'required': Eval('type') == 'distribution',
})
2016-10-05 19:20:00 +02:00
distribution_parents = fields.Many2Many(
'analytic_account.account.distribution', 'account', 'parent',
"Distribution Parents", readonly=True)
2008-05-23 15:04:38 +02:00
2012-09-24 22:11:39 +02:00
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
2012-09-24 22:11:39 +02:00
super(Account, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.add(
Index(t, (t.code, Index.Similarity())))
2012-09-24 22:11:39 +02:00
cls._order.insert(0, ('code', 'ASC'))
2016-09-02 10:23:01 +02:00
cls._order.insert(1, ('name', 'ASC'))
2008-05-23 15:04:38 +02:00
2016-09-27 20:55:23 +02:00
@classmethod
def __register__(cls, module_name):
super(Account, cls).__register__(module_name)
2018-07-24 22:01:07 +02:00
table = cls.__table_handler__(module_name)
2016-09-27 20:55:23 +02:00
2019-03-24 18:00:01 +01:00
# Migration from 5.0: remove display_balance
table.drop_column('display_balance')
2012-09-24 22:11:39 +02:00
@staticmethod
def default_company():
2012-03-27 10:43:17 +02:00
return Transaction().context.get('company')
2008-05-23 15:04:38 +02:00
2012-09-24 22:11:39 +02:00
@staticmethod
def default_type():
2008-05-23 15:04:38 +02:00
return 'normal'
2012-09-24 22:11:39 +02:00
@staticmethod
def default_state():
2008-05-23 15:04:38 +02:00
return 'draft'
@classmethod
2022-04-11 23:24:21 +02:00
def validate_fields(cls, accounts, field_names):
super().validate_fields(accounts, field_names)
cls.check_distribution(accounts, field_names)
cls.check_move_domain(accounts, field_names)
2016-10-05 19:20:00 +02:00
2022-04-11 23:24:21 +02:00
@classmethod
def check_distribution(cls, accounts, field_names=None):
if field_names and not (field_names & {'distributions', 'type'}):
2016-10-05 19:20:00 +02:00
return
2022-04-11 23:24:21 +02:00
for account in accounts:
if account.type != 'distribution':
return
if sum((d.ratio for d in account.distributions)) != 1:
raise AccountValidationError(
gettext('analytic_account.msg_invalid_distribution',
account=account.rec_name))
@classmethod
def check_move_domain(cls, accounts, field_names):
pool = Pool()
2023-09-12 18:39:15 +02:00
Line = pool.get('analytic_account.line')
if field_names and 'type' not in field_names:
return
accounts = [
a for a in accounts if a.type in {'root', 'view', 'distribution'}]
for sub_accounts in grouped_slice(accounts):
sub_accounts = list(sub_accounts)
lines = Line.search([
('account', 'in', [a.id for a in sub_accounts]),
], order=[], limit=1)
if lines:
line, = lines
raise AccountValidationError(gettext(
'analytic_account.msg_account_wrong_type_line',
account=line.account.rec_name))
2016-09-27 20:55:23 +02:00
@fields.depends('company')
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.company.currency if self.company else None
2016-09-27 20:55:23 +02:00
2016-09-28 00:13:29 +02:00
@fields.depends('parent', 'type',
2020-05-03 14:37:58 +02:00
'_parent_parent.id', '_parent_parent.root', '_parent_parent.type')
2016-09-28 00:13:29 +02:00
def on_change_parent(self):
2020-05-03 14:37:58 +02:00
if (self.parent and self.parent.id is not None and self.parent.id > 0
and self.type != 'root'):
2016-09-28 00:13:29 +02:00
if self.parent.type == 'root':
self.root = self.parent
else:
self.root = self.parent.root
else:
self.root = None
2012-09-24 22:11:39 +02:00
@classmethod
def get_balance(cls, accounts, name):
2013-09-06 22:33:59 +02:00
pool = Pool()
Line = pool.get('analytic_account.line')
MoveLine = pool.get('account.move.line')
cursor = Transaction().connection.cursor()
2013-09-06 22:33:59 +02:00
table = cls.__table__()
line = Line.__table__()
move_line = MoveLine.__table__()
2008-05-23 15:04:38 +02:00
2012-09-24 22:11:39 +02:00
ids = [a.id for a in accounts]
childs = cls.search([('parent', 'child_of', ids)])
2018-06-26 19:11:45 +02:00
all_ids = list({}.fromkeys(ids + [c.id for c in childs]).keys())
2008-05-23 15:04:38 +02:00
id2account = {}
2012-09-24 22:11:39 +02:00
all_accounts = cls.browse(all_ids)
for account in all_accounts:
2008-05-23 15:04:38 +02:00
id2account[account.id] = account
2013-09-06 22:33:59 +02:00
line_query = Line.query_get(line)
cursor.execute(*table.join(line, 'LEFT',
condition=table.id == line.account
).join(move_line, 'LEFT',
condition=move_line.id == line.move_line
).select(table.id,
2019-03-24 18:00:01 +01:00
Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0)),
2013-09-06 22:33:59 +02:00
where=(table.type != 'view')
& table.id.in_(all_ids)
2020-03-01 12:33:52 +01:00
& (table.active == Literal(True)) & line_query,
2016-09-27 20:55:23 +02:00
group_by=table.id))
2023-05-11 16:49:58 +02:00
values = defaultdict(Decimal)
for account_id, value in cursor:
# SQLite uses float for SUM
2016-09-27 20:55:23 +02:00
if not isinstance(value, Decimal):
value = Decimal(str(value))
2023-05-11 16:49:58 +02:00
values[account_id] += value
2008-05-23 15:04:38 +02:00
2023-05-11 16:49:58 +02:00
balances = sum_tree(childs, values)
2016-09-27 20:55:23 +02:00
for account in accounts:
2023-05-11 16:49:58 +02:00
balances[account.id] = account.currency.round(balances[account.id])
2016-09-27 20:55:23 +02:00
return balances
2008-05-23 15:04:38 +02:00
2012-09-24 22:11:39 +02:00
@classmethod
def get_credit_debit(cls, accounts, names):
2012-09-24 22:11:39 +02:00
pool = Pool()
Line = pool.get('analytic_account.line')
2013-09-06 22:33:59 +02:00
MoveLine = pool.get('account.move.line')
cursor = Transaction().connection.cursor()
2013-09-06 22:33:59 +02:00
table = cls.__table__()
line = Line.__table__()
move_line = MoveLine.__table__()
2008-05-23 15:04:38 +02:00
result = {}
ids = [a.id for a in accounts]
for name in names:
if name not in ('credit', 'debit'):
raise Exception('Bad argument')
result[name] = {}.fromkeys(ids, Decimal(0))
2008-05-23 15:04:38 +02:00
id2account = {}
for account in accounts:
id2account[account.id] = account
2013-09-06 22:33:59 +02:00
line_query = Line.query_get(line)
2016-09-27 20:55:23 +02:00
columns = [table.id]
for name in names:
columns.append(Sum(Coalesce(Column(line, name), 0)))
2013-09-06 22:33:59 +02:00
cursor.execute(*table.join(line, 'LEFT',
condition=table.id == line.account
).join(move_line, 'LEFT',
condition=move_line.id == line.move_line
).select(*columns,
2013-09-06 22:33:59 +02:00
where=(table.type != 'view')
& table.id.in_(ids)
2020-03-01 12:33:52 +01:00
& (table.active == Literal(True)) & line_query,
2016-09-27 20:55:23 +02:00
group_by=table.id))
2008-05-23 15:04:38 +02:00
for row in cursor:
2016-09-27 20:55:23 +02:00
account_id = row[0]
for i, name in enumerate(names, 1):
value = row[i]
# SQLite uses float for SUM
2016-09-27 20:55:23 +02:00
if not isinstance(value, Decimal):
value = Decimal(str(value))
result[name][account_id] += value
for account in accounts:
for name in names:
2021-08-21 09:09:55 +02:00
result[name][account.id] = account.currency.round(
result[name][account.id])
return result
2008-05-23 15:04:38 +02:00
2012-09-24 22:11:39 +02:00
def get_rec_name(self, name):
if self.code:
2018-06-26 19:11:45 +02:00
return self.code + ' - ' + str(self.name)
2012-09-24 22:11:39 +02:00
else:
2018-06-26 19:11:45 +02:00
return str(self.name)
2008-05-23 15:04:38 +02:00
2012-09-24 22:11:39 +02:00
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
if operator.startswith('!') or operator.startswith('not '):
2021-03-06 19:25:16 +01:00
bool_op = 'AND'
else:
2021-03-06 19:25:16 +01:00
bool_op = 'OR'
code_value = operand
if operator.endswith('like') and is_full_text(operand):
code_value = lstrip_wildcard(operand)
2021-03-06 19:25:16 +01:00
return [bool_op,
('code', operator, code_value, *extra),
(cls._rec_name, operator, operand, *extra),
2021-03-06 19:25:16 +01:00
]
2008-05-23 15:04:38 +02:00
2016-10-05 19:20:00 +02:00
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, current_amount = result[i]
2016-10-05 19:20:00 +02:00
rounding = self.currency.rounding.copy_sign(remainder)
result[i] = (account, current_amount + rounding)
2016-10-05 19:20:00 +02:00
remainder -= rounding
i = (i + 1) % len(result)
assert sum(a for _, a in result) == amount
2016-10-05 19:20:00 +02:00
return result
@classmethod
def write(cls, *args):
pool = Pool()
Entry = pool.get('analytic.account.entry')
actions = iter(args)
for records, values in zip(actions, actions):
if 'root' in values:
for sub_records in grouped_slice(records):
entries = Entry.search([
('account', 'in', list(map(int, sub_records))),
],
limit=1, order=[])
if entries:
entry, = entries
raise AccessError(gettext(
'analytic_account'
'.msg_analytic_account_root_change',
account=entry.account.rec_name))
super().write(*args)
2008-05-23 15:04:38 +02:00
2022-07-18 00:04:04 +02:00
class AccountContext(ModelView):
2012-01-02 18:00:00 +01:00
'Open Chart of Accounts'
2022-07-18 00:04:04 +02:00
__name__ = 'analytic_account.account.context'
2008-05-23 15:04:38 +02:00
start_date = fields.Date('Start Date')
end_date = fields.Date('End Date')
2016-10-05 19:20:00 +02:00
class AccountDistribution(ModelView, ModelSQL):
"Analytic Account Distribution"
__name__ = 'analytic_account.account.distribution'
parent = fields.Many2One(
'analytic_account.account', "Parent", required=True)
2016-10-05 19:20:00 +02:00
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)),
])
2016-10-05 19:20:00 +02:00
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):
2023-03-26 09:49:00 +02:00
return self.parent.root if self.parent else None
2016-10-05 19:20:00 +02:00
2015-01-02 14:04:55 +01:00
class AnalyticAccountEntry(ModelView, ModelSQL):
'Analytic Account Entry'
__name__ = 'analytic.account.entry'
origin = fields.Reference('Origin', selection='get_origin')
2017-04-21 10:19:16 +02:00
root = fields.Many2One(
'analytic_account.account', "Root Analytic", required=True,
2016-09-27 19:36:00 +02:00
domain=[
If(~Eval('company'),
# No constraint if the origin is not set
(),
('company', '=', Eval('company', -1))),
('type', '=', 'root'),
])
2015-01-02 14:04:55 +01:00
account = fields.Many2One('analytic_account.account', 'Account',
ondelete='RESTRICT',
2015-01-02 14:04:55 +01:00
domain=[
('root', '=', Eval('root')),
2016-10-05 19:20:00 +02:00
('type', 'in', ['normal', 'distribution']),
])
2016-09-27 19:36:00 +02:00
company = fields.Function(fields.Many2One('company.company', 'Company'),
'on_change_with_company', searcher='search_company')
2008-05-26 11:46:01 +02:00
2012-09-24 22:11:39 +02:00
@classmethod
def __setup__(cls):
2015-01-02 14:04:55 +01:00
super(AnalyticAccountEntry, cls).__setup__()
t = cls.__table__()
2015-01-02 14:04:55 +01:00
cls._sql_constraints += [
('root_origin_uniq', Unique(t, t.origin, t.root),
2018-12-29 14:20:29 +01:00
'analytic_account.msg_root_origin_unique'),
2015-01-02 14:04:55 +01:00
]
cls._sql_indexes.add(Index(t, (t.origin, Index.Equality())))
2015-01-02 14:04:55 +01:00
@classmethod
def _get_origin(cls):
2017-01-07 16:46:24 +01:00
return ['analytic_account.rule']
2015-01-02 14:04:55 +01:00
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
2020-12-27 00:19:30 +01:00
get_name = Model.get_name
2015-01-02 14:04:55 +01:00
models = cls._get_origin()
2020-12-27 00:19:30 +01:00
return [(None, '')] + [(m, get_name(m)) for m in models]
2015-01-02 14:04:55 +01:00
2016-09-27 19:36:00 +02:00
def on_change_with_company(self, name=None):
return None
@classmethod
def search_company(cls, name, clause):
return []
2016-10-05 19:20:00 +02:00
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
2015-01-02 14:04:55 +01:00
2016-10-12 22:35:00 +02:00
class AnalyticMixin(object):
2019-09-07 23:45:08 +02:00
__slots__ = ()
2015-01-02 14:04:55 +01:00
analytic_accounts = fields.One2Many('analytic.account.entry', 'origin',
'Analytic Accounts',
size=Eval('analytic_accounts_size', 0))
2015-01-02 14:04:55 +01:00
analytic_accounts_size = fields.Function(fields.Integer(
'Analytic Accounts Size'), 'get_analytic_accounts_size')
2016-09-27 19:36:00 +02:00
@classmethod
def analytic_accounts_domain(cls):
context = Transaction().context.copy()
context['context'] = context
return PYSONDecoder(context).decode(
PYSONEncoder().encode(cls.analytic_accounts.domain))
@classmethod
def default_analytic_accounts(cls):
2015-01-02 14:04:55 +01:00
pool = Pool()
AnalyticAccount = pool.get('analytic_account.account')
accounts = []
2016-09-27 19:36:00 +02:00
root_accounts = AnalyticAccount.search(
cls.analytic_accounts_domain() + [
2015-01-02 14:04:55 +01:00
('parent', '=', None),
])
for account in root_accounts:
accounts.append({
'root': account.id,
})
return accounts
2016-09-27 19:36:00 +02:00
@classmethod
def default_analytic_accounts_size(cls):
2015-01-02 14:04:55 +01:00
pool = Pool()
AnalyticAccount = pool.get('analytic_account.account')
2016-09-27 19:36:00 +02:00
return len(AnalyticAccount.search(
cls.analytic_accounts_domain() + [
('type', '=', 'root'),
]))
2012-09-24 22:11:39 +02:00
@classmethod
2015-01-02 14:04:55 +01:00
def get_analytic_accounts_size(cls, records, name):
roots = cls.default_analytic_accounts_size()
return {r.id: roots for r in records}