492 lines
17 KiB
Python
492 lines
17 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
from decimal import Decimal
|
|
from xml.sax.saxutils import quoteattr
|
|
|
|
from simpleeval import InvalidExpression, simple_eval
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
DeactivableMixin, ModelSQL, ModelView, fields, sequence_ordered)
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Eval
|
|
from trytond.tools import decistmt
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import (
|
|
Button, StateAction, StateTransition, StateView, Wizard)
|
|
|
|
from .exceptions import (
|
|
MoveTemplateExpressionError, MoveTemplateKeywordValidationError,
|
|
PeriodNotFoundError)
|
|
|
|
|
|
class MoveTemplate(DeactivableMixin, ModelSQL, ModelView):
|
|
__name__ = 'account.move.template'
|
|
name = fields.Char('Name', required=True, translate=True)
|
|
keywords = fields.One2Many('account.move.template.keyword', 'move',
|
|
'Keywords')
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
journal = fields.Many2One(
|
|
'account.journal', 'Journal', required=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends={'company'})
|
|
description = fields.Char('Description',
|
|
help="Keyword value substitutions are identified "
|
|
"by braces ('{' and '}').")
|
|
lines = fields.One2Many('account.move.line.template', 'move', 'Lines',
|
|
domain=[
|
|
('account.company', '=', Eval('company', -1)),
|
|
])
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def validate_fields(cls, templates, field_names):
|
|
super().validate_fields(templates, field_names)
|
|
cls.check_description(templates, field_names)
|
|
|
|
@classmethod
|
|
def check_description(cls, templates, field_names=None):
|
|
pool = Pool()
|
|
Keyword = pool.get('account.move.template.keyword')
|
|
if field_names and not (field_names & {'description', 'keywords'}):
|
|
return
|
|
for template in templates:
|
|
if template.description:
|
|
values = {k.name: '' for k in template.keywords}
|
|
try:
|
|
template.description.format(
|
|
**dict(Keyword.format_values(template, values)))
|
|
except (KeyError, ValueError) as e:
|
|
raise MoveTemplateKeywordValidationError(
|
|
gettext(
|
|
'account.msg_move_template_invalid_description',
|
|
description=template.description,
|
|
template=template.rec_name,
|
|
error=e)) from e
|
|
|
|
def get_move(self, values):
|
|
'Return the move for the keyword values'
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Keyword = pool.get('account.move.template.keyword')
|
|
|
|
move = Move()
|
|
move.company = self.company
|
|
move.journal = self.journal
|
|
if self.description:
|
|
try:
|
|
move.description = self.description.format(
|
|
**dict(Keyword.format_values(self, values)))
|
|
except (KeyError, ValueError) as e:
|
|
raise MoveTemplateExpressionError(
|
|
gettext(
|
|
'account.msg_move_template_invalid_description',
|
|
description=self.description,
|
|
template=self.rec_name,
|
|
error=e)) from e
|
|
move.lines = [l.get_line(values) for l in self.lines]
|
|
|
|
return move
|
|
|
|
|
|
class MoveTemplateKeyword(sequence_ordered(), ModelSQL, ModelView):
|
|
__name__ = 'account.move.template.keyword'
|
|
name = fields.Char('Name', required=True)
|
|
string = fields.Char('String', required=True, translate=True)
|
|
move = fields.Many2One(
|
|
'account.move.template', "Move", required=True, ondelete='CASCADE')
|
|
type_ = fields.Selection([
|
|
('char', 'Char'),
|
|
('numeric', 'Numeric'),
|
|
('date', 'Date'),
|
|
('party', 'Party'),
|
|
], 'Type')
|
|
required = fields.Boolean('Required')
|
|
digits = fields.Integer('Digits', states={
|
|
'invisible': Eval('type_') != 'numeric',
|
|
'required': Eval('type_') == 'numeric',
|
|
})
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('move')
|
|
|
|
@classmethod
|
|
def validate(cls, keywords):
|
|
for keyword in keywords:
|
|
keyword.check_name()
|
|
|
|
def check_name(self):
|
|
if self.name and not self.name.isidentifier():
|
|
raise MoveTemplateKeywordValidationError(
|
|
gettext('account.msg_name_not_valid',
|
|
name=self.name))
|
|
|
|
@staticmethod
|
|
def default_required():
|
|
return False
|
|
|
|
def get_field(self):
|
|
field = getattr(self, '_get_field_%s' % self.type_)()
|
|
field.update({
|
|
'name': self.name,
|
|
'string': self.string,
|
|
'required': self.required,
|
|
'help': '',
|
|
})
|
|
return field
|
|
|
|
def _get_field_char(self):
|
|
return {'type': 'char'}
|
|
|
|
def _get_field_numeric(self):
|
|
return {'type': 'numeric', 'digits': (16, self.digits)}
|
|
|
|
def _format_numeric(self, lang, value):
|
|
if value:
|
|
return lang.format('%.*f', (self.digits, value), True)
|
|
else:
|
|
return ''
|
|
|
|
def _get_field_date(self):
|
|
return {'type': 'date'}
|
|
|
|
def _format_date(self, lang, value):
|
|
if value:
|
|
return lang.strftime(value)
|
|
else:
|
|
return ''
|
|
|
|
def _get_field_party(self):
|
|
return {
|
|
'type': 'many2one',
|
|
'relation': 'party.party',
|
|
}
|
|
|
|
def _format_party(self, lang, value):
|
|
pool = Pool()
|
|
Party = pool.get('party.party')
|
|
if value:
|
|
return Party(value).rec_name
|
|
else:
|
|
return ''
|
|
|
|
@staticmethod
|
|
def format_values(template, values):
|
|
"Yield key and formatted value"
|
|
pool = Pool()
|
|
Lang = pool.get('ir.lang')
|
|
|
|
lang, = Lang.search([
|
|
('code', '=', Transaction().language),
|
|
])
|
|
keywords = {k.name: k for k in template.keywords}
|
|
|
|
for k, v in values.items():
|
|
keyword = keywords[k]
|
|
func = getattr(keyword, '_format_%s' % keyword.type_, None)
|
|
if func:
|
|
yield k, func(lang, v)
|
|
else:
|
|
yield k, v
|
|
|
|
|
|
class MoveLineTemplate(ModelSQL, ModelView):
|
|
__name__ = 'account.move.line.template'
|
|
move = fields.Many2One(
|
|
'account.move.template', "Move", required=True, ondelete='CASCADE')
|
|
operation = fields.Selection([
|
|
('debit', 'Debit'),
|
|
('credit', 'Credit'),
|
|
], 'Operation', required=True)
|
|
amount = fields.Char('Amount', required=True,
|
|
help="A python expression that will be evaluated with the keywords.")
|
|
account = fields.Many2One('account.account', 'Account', required=True,
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('_parent_move', {}).get('company', -1)),
|
|
])
|
|
party = fields.Char('Party',
|
|
states={
|
|
'required': Eval('party_required', False),
|
|
'invisible': ~Eval('party_required', False),
|
|
},
|
|
help="The name of the 'Party' keyword.")
|
|
party_required = fields.Function(fields.Boolean('Party Required'),
|
|
'on_change_with_party_required')
|
|
description = fields.Char('Description',
|
|
help="Keyword value substitutions are identified "
|
|
"by braces ('{' and '}').")
|
|
taxes = fields.One2Many('account.tax.line.template', 'line', 'Taxes')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('move')
|
|
|
|
@fields.depends('account')
|
|
def on_change_with_party_required(self, name=None):
|
|
if self.account:
|
|
return self.account.party_required
|
|
return False
|
|
|
|
def get_line(self, values):
|
|
'Return the move line for the keyword values'
|
|
pool = Pool()
|
|
Line = pool.get('account.move.line')
|
|
Keyword = pool.get('account.move.template.keyword')
|
|
|
|
line = Line()
|
|
try:
|
|
amount = simple_eval(decistmt(self.amount),
|
|
functions={'Decimal': Decimal}, names=values)
|
|
except (InvalidExpression, SyntaxError) as e:
|
|
raise MoveTemplateExpressionError(
|
|
gettext('account.msg_move_template_invalid_expression',
|
|
expression=values,
|
|
template=self.move.rec_name,
|
|
error=e)) from e
|
|
|
|
if not isinstance(amount, Decimal):
|
|
raise MoveTemplateExpressionError(
|
|
gettext('account.msg_move_template_expression_not_number',
|
|
value=amount,
|
|
expression=self.move.name,
|
|
template=self.move.rec_name))
|
|
|
|
amount = self.move.company.currency.round(amount)
|
|
if self.operation == 'debit':
|
|
line.debit = amount
|
|
else:
|
|
line.credit = amount
|
|
line.account = self.account
|
|
if self.party:
|
|
line.party = values.get(self.party)
|
|
if self.description:
|
|
try:
|
|
line.description = self.description.format(
|
|
**dict(Keyword.format_values(self.move, values)))
|
|
except KeyError as e:
|
|
raise MoveTemplateExpressionError(
|
|
gettext('account.msg_move_template_invalid_expression',
|
|
expression=values,
|
|
template=self.move.name,
|
|
error=e)) from e
|
|
line.tax_lines = [t.get_line(values) for t in self.taxes]
|
|
|
|
return line
|
|
|
|
|
|
class TaxLineTemplate(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.line.template'
|
|
line = fields.Many2One(
|
|
'account.move.line.template', "Line",
|
|
required=True, ondelete='CASCADE')
|
|
amount = fields.Char('Amount', required=True,
|
|
help="A python expression that will be evaluated with the keywords.")
|
|
type = fields.Selection([
|
|
('tax', "Tax"),
|
|
('base', "Base"),
|
|
], "Type", required=True)
|
|
|
|
tax = fields.Many2One('account.tax', 'Tax',
|
|
domain=[
|
|
('company', '=', Eval('_parent_line', {}
|
|
).get('_parent_move', {}).get('company', -1)),
|
|
],
|
|
depends={'line'})
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('line')
|
|
|
|
def get_line(self, values):
|
|
'Return the tax line for the keyword values'
|
|
pool = Pool()
|
|
TaxLine = pool.get('account.tax.line')
|
|
|
|
line = TaxLine()
|
|
try:
|
|
amount = simple_eval(decistmt(self.amount),
|
|
functions={'Decimal': Decimal}, names=values)
|
|
except (InvalidExpression, SyntaxError) as e:
|
|
raise MoveTemplateExpressionError(
|
|
gettext('account.msg_template_invalid_expression',
|
|
expression=values,
|
|
template=self.line.rec_name,
|
|
error=e)) from e
|
|
|
|
if not isinstance(amount, Decimal):
|
|
raise MoveTemplateExpressionError(
|
|
gettext('account.msg_not_number',
|
|
result=amount,
|
|
expression=self.move.rec_name))
|
|
amount = self.line.move.company.currency.round(amount)
|
|
line.amount = amount
|
|
line.type = self.type
|
|
line.tax = self.tax
|
|
return line
|
|
|
|
|
|
class KeywordStateView(StateView):
|
|
|
|
def get_view(self, wizard, state_name):
|
|
fields = {}
|
|
view = {
|
|
'model': 'account.move.template.create.keywords',
|
|
'view_id': 0,
|
|
'type': 'form',
|
|
'fields': fields,
|
|
}
|
|
if not wizard.template.template:
|
|
return view
|
|
template = wizard.template.template
|
|
field_template = ('<label name=%(name)s/>'
|
|
'<field name=%(name)s/>')
|
|
view['arch'] = ('<?xml version="1.0"?>'
|
|
'<form col="2" string=%s>%s</form>' % (
|
|
quoteattr(template.name),
|
|
''.join(field_template % {'name': quoteattr(keyword.name)}
|
|
for keyword in template.keywords)
|
|
))
|
|
for keyword in template.keywords:
|
|
fields[keyword.name] = keyword.get_field()
|
|
return view
|
|
|
|
def get_defaults(self, wizard, state_name, fields):
|
|
return {}
|
|
|
|
|
|
class CreateMove(Wizard):
|
|
__name__ = 'account.move.template.create'
|
|
start = StateTransition()
|
|
template = StateView('account.move.template.create.template',
|
|
'account.move_template_create_template_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Next', 'keywords', 'tryton-forward', default=True),
|
|
])
|
|
keywords = KeywordStateView('account.move.template.create.keywords',
|
|
None, [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Create', 'create_', 'tryton-ok', default=True),
|
|
])
|
|
create_ = StateTransition()
|
|
open_ = StateAction('account.act_move_from_template')
|
|
|
|
def create_move(self):
|
|
template = self.template.template
|
|
values = {}
|
|
for keyword in template.keywords:
|
|
values[keyword.name] = getattr(self.keywords, keyword.name, None)
|
|
move = template.get_move(values)
|
|
move.date = self.template.date
|
|
move.period = self.template.period
|
|
move.save()
|
|
return move
|
|
|
|
def transition_start(self):
|
|
context = Transaction().context
|
|
action_id = context.get('action_id')
|
|
period = context.get('period')
|
|
if self.model and self.model.__name__ == 'account.move.line':
|
|
# Template id is used as action
|
|
self.template.template = action_id
|
|
self.template.period = period
|
|
return 'keywords'
|
|
else:
|
|
return 'template'
|
|
|
|
def transition_create_(self):
|
|
if self.model and self.model.__name__ == 'account.move.line':
|
|
self.create_move()
|
|
return 'end'
|
|
else:
|
|
return 'open_'
|
|
|
|
def do_open_(self, action):
|
|
move = self.create_move()
|
|
return action, {'res_id': move.id}
|
|
|
|
def end(self):
|
|
if self.model and self.model.__name__ == 'account.move.line':
|
|
return 'reload'
|
|
|
|
|
|
class CreateMoveTemplate(ModelView):
|
|
__name__ = 'account.move.template.create.template'
|
|
template = fields.Many2One('account.move.template', 'Template',
|
|
required=True,
|
|
domain=[
|
|
('company', '=', Eval('context', {}).get('company', -1)),
|
|
])
|
|
date = fields.Date('Effective Date', required=True)
|
|
period = fields.Many2One('account.period', 'Period', required=True,
|
|
domain=[
|
|
('state', '!=', 'closed'),
|
|
('fiscalyear.company.id', '=',
|
|
Eval('context', {}).get('company', 0)),
|
|
])
|
|
|
|
@classmethod
|
|
def default_date(cls):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
return Date.today()
|
|
|
|
@classmethod
|
|
def default_period(cls):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
company = Transaction().context.get('company')
|
|
try:
|
|
period = Period.find(company)
|
|
except PeriodNotFoundError:
|
|
return None
|
|
return period.id
|
|
|
|
@fields.depends('date', 'period')
|
|
def on_change_date(self):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
company = Transaction().context.get('company')
|
|
if self.date:
|
|
if (not self.period
|
|
or not (
|
|
self.period.start_date <= self.date
|
|
<= self.period.end_date)):
|
|
try:
|
|
self.period = Period.find(company, date=self.date)
|
|
except PeriodNotFoundError:
|
|
pass
|
|
|
|
@fields.depends('period', 'date')
|
|
def on_change_period(self):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
today = Date.today()
|
|
if self.period:
|
|
start_date = self.period.start_date
|
|
end_date = self.period.end_date
|
|
if (not self.date
|
|
or not (start_date <= self.date <= end_date)):
|
|
if start_date <= today:
|
|
if today <= end_date:
|
|
self.date = today
|
|
else:
|
|
self.date = end_date
|
|
else:
|
|
self.date = start_date
|
|
|
|
|
|
class CreateMoveKeywords(ModelView):
|
|
__no_slots__ = True
|
|
__name__ = 'account.move.template.create.keywords'
|