trytond-analytic_line_state/account.py

284 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 Model, ModelView, fields, dualmethod
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Bool
from trytond.i18n import gettext
from trytond.exceptions import UserError
from trytond.model.exceptions import AccessError
from trytond.transaction import Transaction
__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':~Bool(Eval('type')),
},
depends=['company', 'analytic_forbidden', 'analytic_optional', 'type'])
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':~Bool(Eval('type')),
},
depends=['company', 'analytic_required', 'analytic_optional', 'type'])
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':~Bool(Eval('type')),
},
depends=['company', 'analytic_required', 'analytic_forbidden', 'type'])
analytic_pending_accounts = fields.Function(
fields.Many2Many('analytic_account.account', None, None,
'Pending Accounts', states={
'invisible':~Bool(Eval('type')),
}, depends=['type']),
'on_change_with_analytic_pending_accounts')
@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:
raise UserError(gettext(
'analytic_line_state.analytic_account_required_forbidden',
account=self.rec_name,
roots=', '.join(a.rec_name
for a in (required & forbidden))
))
if required & optional:
raise UserError(gettext(
'analytic_line_state.analytic_account_required_optional',
account=self.rec_name,
roots=', '.join(a.rec_name
for a in (required & optional))
))
if forbidden & optional:
raise UserError(gettext(
'analytic_line_state.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
@ModelView.button
def post(cls, moves):
super(Move, cls).post(moves)
for move in moves:
origin = ''
origin_model = ''
# Ensure that "move.origin" is an instance because if a reference
# field is assigned a model name but not an id "move.origin" will
# return an str
if move.origin and isinstance(move.origin, Model):
origin = move.origin.rec_name
origin_model = move.origin.__names__().get('model')
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:
raise UserError(gettext(
'analytic_line_state.missing_analytic_lines',
move=move.rec_name,
account="[%s] %s" % (line.account.code,
line.account.name),
roots=', '.join(r.rec_name
for r in required_roots),
origin=origin,
origin_model=origin_model,
))
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'):
raise UserError(gettext(
'analytic_line_state.invalid_analytic_to_post_move',
move=move.rec_name,
line=line.rec_name,
root=analytic_line.account.root.rec_name,
))
if required_roots:
raise UserError(gettext(
'analytic_line_state.missing_analytic_lines',
move=move.rec_name,
account="[%s] %s" % (line.account.code,
line.account.name),
roots=', '.join(r.rec_name
for r in required_roots),
origin=origin,
origin_model=origin_model,
))
class MoveLine(metaclass=PoolMeta):
__name__ = 'account.move.line'
@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:
raise UserError(gettext(
'analytic_line_state.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',
})
from_statement = Transaction().context.get(
'from_account_bank_statement_line', False)
if not from_statement:
for line in lines:
if line.move.state == 'posted':
raise AccessError(gettext(
'account.msg_modify_line_posted_move',
line=line.rec_name,
move=line.move.rec_name,
))
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])