trytond-analytic_line_state/account.py
2018-12-25 10:25:20 +01:00

290 lines
11 KiB
Python

# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from itertools import chain
from trytond.model import ModelView, fields, dualmethod
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
__all__ = ['Configuration', 'Account', 'Move', 'MoveLine']
class Configuration(metaclass=PoolMeta):
__name__ = 'account.configuration'
validate_analytic = fields.Boolean('Validate Analytic',
help='If marked it will prevent to post a move to an account that '
'has Pending Analytic accounts.')
class Account(metaclass=PoolMeta):
__name__ = 'account.account'
analytic_required = fields.Many2Many(
'analytic_account.account-required-account.account', 'account',
'analytic_account', 'Analytic Required', domain=[
('type', '=', 'root'),
('company', '=', Eval('company')),
('id', 'not in', Eval('analytic_forbidden')),
('id', 'not in', Eval('analytic_optional')),
], states={
'invisible': Eval('kind') == 'view',
},
depends=['company', 'analytic_forbidden', 'analytic_optional', 'kind'])
analytic_forbidden = fields.Many2Many(
'analytic_account.account-forbidden-account.account', 'account',
'analytic_account', 'Analytic Forbidden', domain=[
('type', '=', 'root'),
('company', '=', Eval('company')),
('id', 'not in', Eval('analytic_required')),
('id', 'not in', Eval('analytic_optional')),
], states={
'invisible': Eval('kind') == 'view',
},
depends=['company', 'analytic_required', 'analytic_optional', 'kind'])
analytic_optional = fields.Many2Many(
'analytic_account.account-optional-account.account', 'account',
'analytic_account', 'Analytic Optional', domain=[
('type', '=', 'root'),
('company', '=', Eval('company')),
('id', 'not in', Eval('analytic_required')),
('id', 'not in', Eval('analytic_forbidden')),
], states={
'invisible': Eval('kind') == 'view',
},
depends=['company', 'analytic_required', 'analytic_forbidden', 'kind'])
analytic_pending_accounts = fields.Function(
fields.Many2Many('analytic_account.account', None, None,
'Pending Accounts', states={
'invisible': Eval('kind') == 'view',
}, depends=['kind']),
'on_change_with_analytic_pending_accounts')
@classmethod
def __setup__(cls):
super(Account, cls).__setup__()
cls._error_messages.update({
'analytic_account_required_forbidden': (
'The Account "%(account)s" has configured the next '
'Analytic Roots as Required and Forbidden at once: '
'%(roots)s.'),
'analytic_account_required_optional': (
'The Account "%(account)s" has configured the next '
'Analytic Roots as Required and Optional at once: '
'%(roots)s.'),
'analytic_account_forbidden_optional': (
'The Account "%(account)s" has configured the next '
'Analytic Roots as Forbidden and Optional at once: '
'%(roots)s.'),
})
@fields.depends('analytic_required', 'analytic_forbidden',
'analytic_optional', 'company')
def on_change_with_analytic_pending_accounts(self, name=None):
AnalyticAccount = Pool().get('analytic_account.account')
current_accounts = [x.id for x in self.analytic_required]
current_accounts += [x.id for x in self.analytic_forbidden]
current_accounts += [x.id for x in self.analytic_optional]
pending_accounts = AnalyticAccount.search([
('type', '=', 'root'),
('company', '=', self.company),
('id', 'not in', current_accounts),
])
return [x.id for x in pending_accounts]
def analytic_constraint(self, analytic_account):
if analytic_account.root in self.analytic_required:
return 'required'
elif analytic_account.root in self.analytic_forbidden:
return 'forbidden'
elif analytic_account.root in self.analytic_optional:
return 'optional'
return 'undefined'
@classmethod
def validate(cls, accounts):
super(Account, cls).validate(accounts)
for account in accounts:
account.check_analytic_accounts()
def check_analytic_accounts(self):
required = set(self.analytic_required)
forbidden = set(self.analytic_forbidden)
optional = set(self.analytic_optional)
if required & forbidden:
self.raise_user_error('analytic_account_required_forbidden', {
'account': self.rec_name,
'roots': ', '.join(a.rec_name
for a in (required & forbidden))
})
if required & optional:
self.raise_user_error('analytic_account_required_optional', {
'account': self.rec_name,
'roots': ', '.join(a.rec_name
for a in (required & optional))
})
if forbidden & optional:
self.raise_user_error('analytic_account_forbidden_optional', {
'account': self.rec_name,
'roots': ', '.join(a.rec_name
for a in (forbidden & optional))
})
class Move(metaclass=PoolMeta):
__name__ = 'account.move'
@classmethod
def __setup__(cls):
super(Move, cls).__setup__()
cls._error_messages.update({
'missing_analytic_lines': (
'The Account Move "%(move)s" can\'t be posted because it '
'doesn\'t have analytic lines for the next required '
'analytic hierachies: %(roots)s.'),
'invalid_analytic_to_post_move': (
'The Account Move "%(move)s" can\'t be posted because the '
'Analytic Lines of hierachy "%(root)s" related to Move '
'Line "%(line)s" are not valid.'),
})
@classmethod
@ModelView.button
def post(cls, moves):
super(Move, cls).post(moves)
for move in moves:
if move.period.type == 'adjustment':
continue
for line in move.lines:
required_roots = list(line.account.analytic_required[:])
if not line.analytic_lines and line.account.analytic_required:
cls.raise_user_error('missing_analytic_lines', {
'move': move.rec_name,
'roots': ', '.join(r.rec_name
for r in required_roots),
})
for analytic_line in line.analytic_lines:
if analytic_line.account.root in required_roots:
required_roots.remove(analytic_line.account.root)
constraint = line.account.analytic_constraint(
analytic_line.account)
if (constraint == 'required' and
analytic_line.state != 'valid'):
cls.raise_user_error('invalid_analytic_to_post_move', {
'move': move.rec_name,
'line': line.rec_name,
'root': analytic_line.account.root.rec_name,
})
if required_roots:
cls.raise_user_error('missing_analytic_lines', {
'move': move.rec_name,
'roots': ', '.join(r.rec_name
for r in required_roots),
})
class MoveLine(metaclass=PoolMeta):
__name__ = 'account.move.line'
@classmethod
def __setup__(cls):
super(MoveLine, cls).__setup__()
cls._error_messages.update({
'account_analytic_not_configured': (
'The Move Line "%(line)s" is related to the Account '
'"%(account)s" which is not configured for all Analytic '
'hierarchies.'),
})
@classmethod
def check_modify(cls, lines, modified_fields=None):
'''
Check if the lines can be modified
'''
if modified_fields is None:
return
super(MoveLine, cls).check_modify(lines, modified_fields)
@classmethod
def validate(cls, lines):
super(MoveLine, cls).validate(lines)
for line in lines:
line.check_account_analytic_configuration()
def check_account_analytic_configuration(self):
pool = Pool()
Config = pool.get('account.configuration')
config = Config(1)
if config.validate_analytic:
if self.account.analytic_pending_accounts:
self.raise_user_error('account_analytic_not_configured', {
'line': self.rec_name,
'account': self.account.rec_name,
})
@classmethod
def validate_analytic_lines(cls, lines):
pool = Pool()
AnalyticLine = pool.get('analytic_account.line')
todraft, tovalid = [], []
for line in lines:
analytic_lines_by_root = {}
for analytic_line in line.analytic_lines:
analytic_lines_by_root.setdefault(analytic_line.account.root,
[]).append(analytic_line)
line_balance = line.debit - line.credit
for analytic_lines in analytic_lines_by_root.values():
balance = sum((al.debit - al.credit) for al in analytic_lines)
if balance == line_balance:
tovalid += [al for al in analytic_lines
if al.state != 'valid']
else:
todraft += [al for al in analytic_lines
if al.state != 'draft']
if todraft:
AnalyticLine.write(todraft, {
'state': 'draft',
})
if tovalid:
AnalyticLine.write(tovalid, {
'state': 'valid',
})
@classmethod
def create(cls, vlist):
lines = super(MoveLine, cls).create(vlist)
cls.validate_analytic_lines(lines)
return lines
@classmethod
def write(cls, *args):
super(MoveLine, cls).write(*args)
lines = list(chain(*args[::2]))
cls.validate_analytic_lines(lines)
@classmethod
def delete(cls, lines):
AnalyticLine = Pool().get('analytic_account.line')
todraft_lines = [al for line in lines for al in line.analytic_lines]
AnalyticLine.write(todraft_lines, {
'state': 'draft',
})
super(MoveLine, cls).delete(lines)
@dualmethod
def save(cls, lines):
# XXX: as required move_line is dropped on analytic line,
# this can be called with None value
super(MoveLine, cls).save([x for x in lines if x])
@classmethod
def set_analytic_state(cls, lines):
# XXX: as required move_line is dropped on analytic line,
# this can be called with None value
super(MoveLine, cls).set_analytic_state([x for x in lines if x])