235 lines
9.3 KiB
Python
235 lines
9.3 KiB
Python
# The COPYRIGHT file at the top level of this repository contains the full
|
|
# copyright notices and license terms.
|
|
import logging
|
|
import time
|
|
|
|
from trytond.model import ModelView, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval
|
|
|
|
__all__ = ['Account', 'Move', 'MoveLine']
|
|
__metaclass__ = PoolMeta
|
|
|
|
|
|
class Account:
|
|
__name__ = 'account.account'
|
|
|
|
analytic_required = fields.Many2Many(
|
|
'analytic_account.account-required-account.account', 'account',
|
|
'analytic_account', 'Analytic Required', domain=[
|
|
('type', '=', 'root'),
|
|
('id', 'not in', Eval('analytic_forbidden')),
|
|
('id', 'not in', Eval('analytic_optional')),
|
|
], depends=['analytic_forbidden', 'analytic_optional'])
|
|
analytic_forbidden = fields.Many2Many(
|
|
'analytic_account.account-forbidden-account.account', 'account',
|
|
'analytic_account', 'Analytic Forbidden', domain=[
|
|
('type', '=', 'root'),
|
|
('id', 'not in', Eval('analytic_required')),
|
|
('id', 'not in', Eval('analytic_optional')),
|
|
], depends=['analytic_required', 'analytic_optional'])
|
|
analytic_optional = fields.Many2Many(
|
|
'analytic_account.account-optional-account.account', 'account',
|
|
'analytic_account', 'Analytic Optional', domain=[
|
|
('type', '=', 'root'),
|
|
('id', 'not in', Eval('analytic_required')),
|
|
('id', 'not in', Eval('analytic_forbidden')),
|
|
], depends=['analytic_required', 'analytic_forbidden'])
|
|
analytic_pending_accounts = fields.Function(
|
|
fields.Many2Many('analytic_account.account', None, None,
|
|
'Pending Accounts', on_change_with=['analytic_required',
|
|
'analytic_forbidden', 'analytic_optional']),
|
|
'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.'),
|
|
})
|
|
|
|
def on_change_with_analytic_pending_accounts(self, name=None):
|
|
AnalyticAccount = Pool().get('analytic_account.account')
|
|
|
|
current_accounts = map(int, self.analytic_required)
|
|
current_accounts += map(int, self.analytic_forbidden)
|
|
current_accounts += map(int, self.analytic_optional)
|
|
pending_accounts = AnalyticAccount.search([
|
|
('type', '=', 'root'),
|
|
('id', 'not in', current_accounts),
|
|
])
|
|
return map(int, 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:
|
|
__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 but its Account is requires '
|
|
'analytics for the next 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:
|
|
for line in move.lines:
|
|
if not line.analytic_lines and line.account.analytic_required:
|
|
req_acc_names = [r.rec_name
|
|
for r in line.account.analytic_required]
|
|
cls.raise_user_error('missing_analytic_lines', {
|
|
'move': move.rec_name,
|
|
'roots': ', '.join(req_acc_names),
|
|
})
|
|
|
|
for analytic_line in line.analytic_lines:
|
|
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,
|
|
})
|
|
|
|
|
|
class MoveLine:
|
|
__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 validate(cls, lines):
|
|
super(MoveLine, cls).validate(lines)
|
|
for line in lines:
|
|
line.check_account_analytic_configuration()
|
|
|
|
def check_account_analytic_configuration(self):
|
|
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')
|
|
|
|
start_time = time.time()
|
|
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 root, analytic_lines in analytic_lines_by_root.items():
|
|
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',
|
|
})
|
|
logging.getLogger(cls.__name__).debug(
|
|
"validate_analytic_lines(): %s seconds"
|
|
% (time.time() - start_time))
|
|
return todraft + tovalid
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
lines = super(MoveLine, cls).create(vlist)
|
|
cls.validate_analytic_lines(lines)
|
|
|
|
@classmethod
|
|
def write(cls, lines, vals):
|
|
super(MoveLine, cls).write(lines, vals)
|
|
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]
|
|
super(MoveLine, cls).delete(lines)
|
|
AnalyticLine.write(todraft_lines, {
|
|
'state': 'draft',
|
|
})
|