336 lines
11 KiB
Python
336 lines
11 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 collections import defaultdict
|
|
from decimal import Decimal
|
|
from operator import attrgetter
|
|
|
|
from sql import Cast, Null, Literal
|
|
from sql.aggregate import Count
|
|
from sql.conditionals import Case
|
|
from sql.functions import Substring, Position
|
|
|
|
from trytond.model import ModelView, fields
|
|
from trytond.model.modelsql import convert_from
|
|
from trytond.pool import Pool
|
|
from trytond.report import Report
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Wizard, StateView, StateTransition, StateReport, \
|
|
Button
|
|
|
|
__all__ = ['AEAT111', 'AEAT115', 'AEAT303', 'PrintAEATStart', 'PrintAEAT']
|
|
|
|
|
|
# XXX fix: https://genshi.edgewall.org/ticket/582
|
|
from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
|
|
if not hasattr(ASTCodeGenerator, 'visit_NameConstant'):
|
|
def visit_NameConstant(self, node):
|
|
if node.value is None:
|
|
self._write('None')
|
|
elif node.value is True:
|
|
self._write('True')
|
|
elif node.value is False:
|
|
self._write('False')
|
|
else:
|
|
raise Exception("Unknown NameConstant %r" % (node.value,))
|
|
ASTCodeGenerator.visit_NameConstant = visit_NameConstant
|
|
if not hasattr(ASTTransformer, 'visit_NameConstant'):
|
|
# Re-use visit_Name because _clone is deleted
|
|
ASTTransformer.visit_NameConstant = ASTTransformer.visit_Name
|
|
|
|
|
|
def justify(string, size):
|
|
return string[:size].ljust(size)
|
|
|
|
|
|
def format_decimal(n, include_sign=False):
|
|
if not isinstance(n, Decimal):
|
|
n = Decimal(n)
|
|
sign = ''
|
|
if include_sign:
|
|
sign = 'N' if n < 0 else ''
|
|
return sign + ('{0:.2f}'.format(abs(n))).replace('.', '').rjust(
|
|
17 - len(sign), '0')
|
|
|
|
|
|
def format_integer(n, size=8):
|
|
return ('%d' % n).rjust(size, '0')
|
|
|
|
|
|
def format_percentage(n, size=5):
|
|
return ('{0:.2f}'.format(n)).replace('.', '').rjust(size, '0')
|
|
|
|
|
|
class AEATReport(Report):
|
|
|
|
@classmethod
|
|
def get_context(cls, records, data):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
context = super().get_context(records, data)
|
|
|
|
periods = sorted(
|
|
Period.browse(data['periods']), key=attrgetter('start_date'))
|
|
|
|
context['year'] = str(periods[0].start_date.year)
|
|
context['company'] = periods[0].fiscalyear.company
|
|
|
|
start_month = periods[0].start_date.month
|
|
end_month = periods[-1].end_date.month
|
|
if end_month - start_month > 0:
|
|
context['period'] = str(end_month // 3) + 'T'
|
|
else:
|
|
context['period'] = str(start_month).rjust(2, '0')
|
|
|
|
context['justify'] = justify
|
|
context['format_decimal'] = format_decimal
|
|
context['format_integer'] = format_integer
|
|
context['format_percentage'] = format_percentage
|
|
|
|
with Transaction().set_context(periods=data['periods']):
|
|
context['amounts'] = cls.compute_amounts()
|
|
|
|
return context
|
|
|
|
@classmethod
|
|
def compute_amounts(cls):
|
|
amounts = defaultdict(lambda: Decimal(0))
|
|
for tax_code in cls.tax_codes():
|
|
amounts[tax_code.code] += tax_code.amount
|
|
return amounts
|
|
|
|
@classmethod
|
|
def tax_codes(cls):
|
|
pool = Pool()
|
|
TaxCode = pool.get('account.tax.code')
|
|
return TaxCode.search([('aeat_report', '=', cls._aeat_report)])
|
|
|
|
|
|
class AEATPartyReport(AEATReport):
|
|
|
|
@classmethod
|
|
def aeat_party_expression(cls, tables):
|
|
'''
|
|
Returns a couple of sql expression and tables used by sql query to
|
|
compute the aeat party.
|
|
'''
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
|
|
table, _ = tables[None]
|
|
is_invoice = table.origin.like(Invoice.__name__ + ',%')
|
|
|
|
if 'invoice' in tables:
|
|
invoice, _ = tables['invoice']
|
|
else:
|
|
invoice = Invoice.__table__()
|
|
tables['invoice'] = {
|
|
None: (invoice, (is_invoice
|
|
& (invoice.id == Cast(
|
|
Substring(table.origin,
|
|
Position(',', table.origin) + Literal(1)),
|
|
Invoice.id.sql_type().base)))),
|
|
}
|
|
|
|
return Case((is_invoice, invoice.party), else_=Null), tables
|
|
|
|
@classmethod
|
|
def get_context(cls, records, data):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
TaxLine = pool.get('account.tax.line')
|
|
Tax = pool.get('account.tax')
|
|
|
|
context = super().get_context(records, data)
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
move = Move.__table__()
|
|
move_line = Line.__table__()
|
|
tax_line = TaxLine.__table__()
|
|
|
|
tables = {
|
|
None: (move, None),
|
|
'lines': {
|
|
None: (move_line, move_line.move == move.id),
|
|
'tax_lines': {
|
|
None: (tax_line, tax_line.move_line == move_line.id),
|
|
},
|
|
},
|
|
}
|
|
|
|
expression, tables = cls.aeat_party_expression(tables)
|
|
|
|
parties = defaultdict(int)
|
|
for tax_code in cls.tax_codes():
|
|
domain = ['OR']
|
|
for line in tax_code.lines:
|
|
domain.append(line._line_domain)
|
|
|
|
with Transaction().set_context(periods=data['periods']):
|
|
tax_line_domain = [Tax._amount_domain(), domain]
|
|
_, where = Move.search_domain([
|
|
('lines', 'where', [
|
|
('tax_lines', 'where', tax_line_domain),
|
|
]),
|
|
], tables=tables)
|
|
|
|
from_ = convert_from(None, tables)
|
|
cursor.execute(*from_.select(
|
|
expression, where=where, group_by=(expression,)).select(
|
|
Count(Literal('*'))))
|
|
row = cursor.fetchone()
|
|
if row:
|
|
parties[tax_code.code] += row[0]
|
|
context['parties'] = parties
|
|
return context
|
|
|
|
|
|
class AEAT111(AEATPartyReport):
|
|
__name__ = 'account.reporting.aeat111'
|
|
_aeat_report = '111'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, data):
|
|
context = super().get_context(records, data)
|
|
amounts = context['amounts']
|
|
for code in ['28', '30']:
|
|
assert code not in amounts, (
|
|
"computed code %s already defined" % code)
|
|
amounts['28'] = (amounts['03'] + amounts['06'] + amounts['09']
|
|
+ amounts['12'] + amounts['15'] + amounts['18'] + amounts['21']
|
|
+ amounts['24'] + amounts['27'])
|
|
amounts['30'] = amounts['28'] - amounts['29']
|
|
return context
|
|
|
|
|
|
class AEAT115(AEATPartyReport):
|
|
__name__ = 'account.reporting.aeat115'
|
|
_aeat_report = '115'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, data):
|
|
context = super().get_context(records, data)
|
|
amounts = context['amounts']
|
|
assert '05' not in amounts, (
|
|
"computed code 05 already defined")
|
|
amounts['05'] = amounts['03'] - amounts['04']
|
|
return context
|
|
|
|
|
|
class AEAT303(AEATReport):
|
|
__name__ = 'account.reporting.aeat303'
|
|
_aeat_report = '303'
|
|
|
|
@classmethod
|
|
def compute_amounts(cls):
|
|
amounts = super().compute_amounts()
|
|
amounts['65'] = 100.0
|
|
return amounts
|
|
|
|
@classmethod
|
|
def get_context(cls, records, data):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
TaxCodeLine = pool.get('account.tax.code.line')
|
|
context = super().get_context(records, data)
|
|
amounts = context['amounts']
|
|
|
|
periods = Period.browse(data['periods'])
|
|
start_date = periods[0].start_date
|
|
end_date = periods[-1].end_date
|
|
|
|
lines = TaxCodeLine.search([
|
|
('code', 'in', cls.tax_codes()),
|
|
('code.code', 'in', ['03', '06', '09', '18', '21', '24']),
|
|
('tax', 'where', [
|
|
('type', '=', 'percentage'),
|
|
['OR',
|
|
('start_date', '=', None),
|
|
('start_date', '<=', end_date),
|
|
],
|
|
['OR',
|
|
('end_date', '=', None),
|
|
('end_date', '>=', start_date),
|
|
],
|
|
]),
|
|
])
|
|
for line in lines:
|
|
code = str(int(line.code.code) - 1).rjust(2, '0')
|
|
amounts[code] = float(line.tax.rate * Decimal(100))
|
|
|
|
for code in ['46', '64', '66', '69', '71', '88']:
|
|
assert code not in amounts, (
|
|
"computed code %s already defined" % code)
|
|
amounts['46'] = amounts['27'] - amounts['45']
|
|
amounts['64'] = amounts['46'] + amounts['58'] + amounts['76']
|
|
amounts['66'] = amounts['64'] * Decimal(amounts['65']) / Decimal(100.0)
|
|
amounts['69'] = (amounts['66'] + amounts['77'] - amounts['67']
|
|
+ amounts['68'])
|
|
amounts['71'] = (amounts['69'] - amounts['70'])
|
|
amounts['88'] = (amounts['80'] + amounts['81'] - amounts['93']
|
|
+ amounts['94'] + amounts['83'] + amounts['84'] + amounts['85']
|
|
+ amounts['86'] + amounts['95'] + amounts['96'] + amounts['97']
|
|
+ amounts['98'] - amounts['79'] - amounts['99'])
|
|
|
|
last_period = [p for p in periods[0].fiscalyear.periods
|
|
if p.type == 'standard'][-1]
|
|
declaration_type = 'N'
|
|
if amounts['69'] > 0:
|
|
declaration_type = 'I'
|
|
elif amounts['69'] < 0:
|
|
declaration_type = 'D' if last_period in periods else 'C'
|
|
context['declaration_type'] = declaration_type
|
|
return context
|
|
|
|
|
|
class PrintAEATStart(ModelView):
|
|
'Print AEAT Start'
|
|
__name__ = 'account.reporting.aeat.start'
|
|
|
|
report = fields.Selection([
|
|
('111', "Model 111"),
|
|
('115', "Model 115"),
|
|
('303', "Model 303"),
|
|
], "Report", required=True)
|
|
periods = fields.Many2Many('account.period', None, None, 'Periods',
|
|
required=True)
|
|
|
|
|
|
class PrintAEAT(Wizard):
|
|
'Print AEAT'
|
|
__name__ = 'account.reporting.aeat'
|
|
start = StateView('account.reporting.aeat.start',
|
|
'account_es.print_aeat_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Print', 'choice', 'tryton-ok', default=True),
|
|
])
|
|
choice = StateTransition()
|
|
model_111 = StateReport('account.reporting.aeat111')
|
|
model_115 = StateReport('account.reporting.aeat115')
|
|
model_303 = StateReport('account.reporting.aeat303')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._error_messages.update({
|
|
'same_fiscalyear': ("Periods should be of the same fiscalyear "
|
|
"to generate the report."),
|
|
})
|
|
|
|
def transition_choice(self):
|
|
validate = getattr(self, 'validate_%s' % self.start.report, None)
|
|
if validate:
|
|
validate()
|
|
return 'model_%s' % self.start.report
|
|
|
|
def open_report(self, action):
|
|
return action, {'periods': [p.id for p in self.start.periods]}
|
|
|
|
do_model_111 = open_report
|
|
do_model_115 = open_report
|
|
do_model_303 = open_report
|
|
|
|
def validate_303(self):
|
|
if len(set(p.fiscalyear for p in self.start.periods)) > 1:
|
|
self.raise_user_error('same_fiscalyear')
|