Files
tradon/modules/account_es/reporting_tax.py
2026-03-14 09:42:12 +00:00

981 lines
36 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.
import csv
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO, TextIOWrapper
from dateutil.relativedelta import relativedelta
# XXX fix: https://genshi.edgewall.org/ticket/582
from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
from sql import Literal, Null
from sql.aggregate import Count, Min, Sum
from sql.conditionals import Case, Coalesce
from sql.functions import CurrentTimestamp, Extract
from sql.operators import Exists
from trytond.i18n import gettext
from trytond.model import ModelSQL, ModelView, convert_from, fields
from trytond.modules.account.exceptions import FiscalYearNotFoundError
from trytond.modules.account_eu.account import ECSalesList, ECSalesListContext
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool
from trytond.pyson import Eval, If
from trytond.report import Report
from trytond.transaction import Transaction
from trytond.wizard import (
Button, StateReport, StateTransition, StateView, Wizard)
from .exceptions import PrintError
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):
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')
def identifier_code(identifier):
if identifier:
return identifier.es_code()
return ''
def country_code(record):
code = None
if record.party_tax_identifier:
code = record.party_tax_identifier.es_country()
if code is None or code == 'ES':
return ''
return code
def strip_accents(s):
return ''.join(c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn')
class AEATReport(Report):
@classmethod
def get_context(cls, periods, header, data):
context = super().get_context(periods, header, data)
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
context['strip_accents'] = strip_accents
with Transaction().set_context(periods=data['ids']):
context['amounts'] = cls.compute_amounts()
return context
@classmethod
def compute_amounts(cls):
amounts = defaultdict(Decimal)
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')
Move = pool.get('account.move')
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 == Move.origin.sql_id(
table.origin, Invoice)))),
}
return Case((is_invoice, invoice.party), else_=Null), tables
@classmethod
def get_context(cls, records, header, 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, header, 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['ids']):
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, header, data):
context = super().get_context(records, header, 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, header, data):
context = super().get_context(records, header, 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, periods, header, data):
pool = Pool()
Account = pool.get('account.account')
TaxCodeLine = pool.get('account.tax.code.line')
transaction = Transaction()
context = super().get_context(periods, header, data)
amounts = context['amounts']
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))
amount_to_compensate = Decimal(0)
fiscalyear = periods[0].fiscalyear
with transaction.set_context({
'fiscalyear': fiscalyear.id,
'to_date': end_date,
}):
for account in Account.search([
('company', '=', fiscalyear.company.id),
('code', 'like', '4700%'),
]):
amount_to_compensate += account.balance
for code in ['46', '64', '66', '67', '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)
amounts['110'] = amounts['78'] = amount_to_compensate
amounts['87'] = amounts['110'] - amounts['78']
amounts['69'] = (amounts['66'] + amounts['77'] - amounts['78']
+ 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):
__name__ = 'account.reporting.aeat.start'
report = fields.Selection([
('111', "Model 111"),
('115', "Model 115"),
('303', "Model 303"),
], "Report", required=True)
company = fields.Many2One('company.company', "Company", required=True)
start_period = fields.Many2One(
'account.period', "Start Period",
required=True,
domain=[
('type', '=', 'standard'),
('fiscalyear.company', '=', Eval('company', -1)),
('start_date', '<=', (Eval('end_period', None), 'start_date')),
])
end_period = fields.Many2One(
'account.period', "End Period",
required=True,
domain=[
('type', '=', 'standard'),
('fiscalyear.company', '=', Eval('company', -1)),
('start_date', '>=', (Eval('start_period', None), 'start_date'))
])
@classmethod
def default_company(cls):
return Transaction().context.get('company')
class PrintAEAT(Wizard):
__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')
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):
pool = Pool()
Period = pool.get('account.period')
periods = Period.search([
('type', '=', 'standard'),
('company', '=', self.start.start_period.fiscalyear.company),
('start_date', '>=', self.start.start_period.start_date),
('end_date', '<=', self.start.end_period.end_date),
],
order=[('start_date', 'ASC')])
return action, {'ids': [p.id for p in periods]}
do_model_111 = open_report
do_model_115 = open_report
do_model_303 = open_report
def validate_303(self):
if (self.start.start_period.fiscalyear
!= self.start.end_period.fiscalyear):
raise PrintError(
gettext('account_es.msg_report_same_fiscalyear'))
class ESVATList(ModelSQL, ModelView):
__name__ = 'account.reporting.vat_list_es'
company_tax_identifier = fields.Many2One(
'party.identifier', "Company Tax Identifier")
party_tax_identifier = fields.Many2One(
'party.identifier', "Party Tax Identifier")
party = fields.Many2One('party.party', "Party")
province_code = fields.Function(fields.Char("Province Code"),
'get_province_code', searcher='search_province_code')
code = fields.Char("Code")
amount = Monetary("Amount", currency='currency', digits='currency')
first_period_amount = Monetary(
"First Period Amount", currency='currency', digits='currency')
second_period_amount = Monetary(
"Second Period Amount", currency='currency', digits='currency')
third_period_amount = Monetary(
"Third Period Amount", currency='currency', digits='currency')
fourth_period_amount = Monetary(
"Fourth Period Amount", currency='currency', digits='currency')
currency = fields.Many2One('currency.currency', "Currency")
@classmethod
def get_province_code(cls, records, name):
return {r.id: r.party.es_province_code or '' if r.party else ''
for r in records}
@classmethod
def search_province_code(cls, name, clause):
return [(('party.es_province_code',) + tuple(clause[1:]))]
@classmethod
def excluded_tax_codes(cls):
return ['111', '115']
@classmethod
def table_query(cls):
pool = Pool()
Company = pool.get('company.company')
Invoice = pool.get('account.invoice')
InvoiceTax = pool.get('account.invoice.tax')
Move = pool.get('account.move')
Line = pool.get('account.move.line')
TaxLine = pool.get('account.tax.line')
Tax = pool.get('account.tax')
TaxCode = pool.get('account.tax.code')
TaxCodeLine = pool.get('account.tax.code.line')
Date = pool.get('ir.date')
context = Transaction().context
company = Company.__table__()
invoice = Invoice.__table__()
cancel_invoice = Invoice.__table__()
move = Move.__table__()
cancel_move = Move.__table__()
line = Line.__table__()
tax_line = TaxLine.__table__()
tax = Tax.__table__()
tax_code = TaxCode.__table__()
tax_code_line = TaxCodeLine.__table__()
exclude_invoice_tax = InvoiceTax.__table__()
amount = tax_line.amount
month = Extract('MONTH', invoice.invoice_date)
excluded_taxes = (tax_code_line
.join(tax_code,
condition=(tax_code.id == tax_code_line.code)
).select(
tax_code_line.tax, distinct=True,
where=tax_code.aeat_report.in_(cls.excluded_tax_codes())))
where = ((invoice.company == context.get('company'))
& (tax.es_vat_list_code != Null)
& (Extract('year', invoice.invoice_date)
== context.get('date', Date.today()).year)
# Exclude base amount for es_reported_with taxes because it is
# already included in the base of main tax
& ((tax.es_reported_with == Null) | (tax_line.type == 'tax'))
& ~Exists(cancel_invoice
.join(cancel_move,
condition=cancel_invoice.cancel_move == cancel_move.id)
.select(cancel_invoice.id, distinct=True,
where=((cancel_invoice.id == invoice.id)
& (~cancel_move.origin.like('account.invoice,%')))))
# Use exists to exclude the full invoice when it has multiple taxes
& ~Exists(exclude_invoice_tax.select(
exclude_invoice_tax.invoice,
where=((exclude_invoice_tax.invoice == invoice.id)
& (exclude_invoice_tax.tax.in_(excluded_taxes))))))
query = (tax_line
.join(tax, condition=tax_line.tax == tax.id)
.join(line, condition=tax_line.move_line == line.id)
.join(move, condition=line.move == move.id)
.join(invoice, condition=invoice.move == move.id)
.join(company, condition=company.id == invoice.company)
.select(
Min(tax_line.id).as_('id'),
invoice.tax_identifier.as_('company_tax_identifier'),
invoice.party.as_('party'),
invoice.party_tax_identifier.as_('party_tax_identifier'),
tax.es_vat_list_code.as_('code'),
Sum(amount).as_('amount'),
Sum(amount, filter_=month <= Literal(3)).as_(
'first_period_amount'),
Sum(amount, filter_=(
(month > Literal(3)) & (month <= Literal(6)))).as_(
'second_period_amount'),
Sum(amount, filter_=(
(month > Literal(6)) & (month <= Literal(9)))).as_(
'third_period_amount'),
Sum(amount, filter_=(
(month > Literal(9)) & (month <= Literal(12)))).as_(
'fourth_period_amount'),
company.currency.as_('currency'),
where=where,
group_by=[
invoice.tax_identifier,
invoice.type,
invoice.party,
invoice.party_tax_identifier,
company.currency,
tax.es_vat_list_code,
]))
return query
class ESVATListContext(ModelView):
__name__ = 'account.reporting.vat_list_es.context'
company = fields.Many2One('company.company', "Company", required=True)
date = fields.Date("Date", required=True,
context={'date_format': '%Y'})
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
def default_date(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()
class AEAT347(Report):
__name__ = 'account.reporting.aeat347'
@classmethod
def get_context(cls, records, header, data):
pool = Pool()
Company = pool.get('company.company')
t_context = Transaction().context
context = super().get_context(records, header, data)
context['year'] = str(t_context['date'].year)
context['company'] = Company(t_context['company'])
context['records_amount'] = sum(
(r.amount for r in records), Decimal(0))
context['justify'] = justify
def format_decimal(n):
sign = 'N' if n < 0 else ' '
return sign + ('{0:.2f}'.format(abs(n))).replace('.', '').rjust(
15, '0')
context['format_decimal'] = format_decimal
context['format_integer'] = format_integer
context['identifier_code'] = identifier_code
context['country_code'] = country_code
context['strip_accents'] = strip_accents
return context
class ECOperationList(ECSalesList):
__name__ = 'account.reporting.es_ec_operation_list'
@classmethod
def table_query(cls):
pool = Pool()
Company = pool.get('company.company')
Invoice = pool.get('account.invoice')
Move = pool.get('account.move')
Line = pool.get('account.move.line')
TaxLine = pool.get('account.tax.line')
Period = pool.get('account.period')
Tax = pool.get('account.tax')
context = Transaction().context
company = Company.__table__()
invoice = Invoice.__table__()
cancel_invoice = Invoice.__table__()
move = Move.__table__()
cancel_move = Move.__table__()
line = Line.__table__()
tax_line = TaxLine.__table__()
period = Period.__table__()
tax = Tax.__table__()
sales = super().table_query()
where = invoice.company == context.get('company')
if context.get('start_date'):
where &= (move.date >= context.get('start_date'))
if context.get('end_date'):
where &= (move.date <= context.get('end_date'))
where &= ((tax.es_ec_purchases_list_code != Null)
& (tax.es_ec_purchases_list_code != ''))
where &= tax_line.type == 'base'
where &= invoice.type == 'in'
where &= ~Exists(cancel_invoice
.join(cancel_move,
condition=cancel_invoice.cancel_move == cancel_move.id)
.select(cancel_invoice.id, distinct=True,
where=((cancel_invoice.id == invoice.id)
& (~cancel_move.origin.like('account.invoice,%')))))
purchases = (tax_line
.join(tax, condition=tax_line.tax == tax.id)
.join(line, condition=tax_line.move_line == line.id)
.join(move, condition=line.move == move.id)
.join(period, condition=move.period == period.id)
.join(invoice, condition=invoice.move == move.id)
.join(company, condition=company.id == invoice.company)
.select(
Min(tax_line.id).as_('id'),
invoice.tax_identifier.as_('company_tax_identifier'),
invoice.party.as_('party'),
invoice.party_tax_identifier.as_('party_tax_identifier'),
tax.es_ec_purchases_list_code.as_('code'),
Sum(tax_line.amount).as_('amount'),
company.currency.as_('currency'),
where=where,
group_by=[
invoice.tax_identifier,
invoice.party,
invoice.party_tax_identifier,
tax.es_ec_purchases_list_code,
company.currency,
]))
return sales | purchases
class ECOperationListContext(ECSalesListContext):
__name__ = 'account.reporting.es_ec_operation_list.context'
start_date = fields.Date("Start Date",
domain=[
If(Eval('end_date'),
('start_date', '<=', Eval('end_date', None)),
(),
),
])
end_date = fields.Date("End Date",
domain=[
If(Eval('start_date'),
('end_date', '>=', Eval('start_date', None)),
(),
),
])
@classmethod
def default_start_date(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today() - relativedelta(months=1, day=1)
@classmethod
def default_end_date(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today() - relativedelta(months=1, day=31)
class AEAT349(Report):
__name__ = 'account.reporting.aeat349'
@classmethod
def get_context(cls, records, header, data):
pool = Pool()
Company = pool.get('company.company')
t_context = Transaction().context
context = super().get_context(records, header, data)
context['company'] = Company(t_context['company'])
context['records_amount'] = sum(
(r.amount for r in records), Decimal(0))
start_date = t_context.get('start_date')
end_date = t_context.get('end_date')
if start_date or end_date:
date = start_date or end_date
context['year'] = str(date.year)
if start_date and end_date:
start_month = start_date.month
end_month = end_date.month
if end_month - start_month > 0:
context['period'] = str(end_month // 3) + 'T'
context['period_number'] = str(20 + (end_month // 3))
else:
context['period'] = str(start_month).rjust(2, '0')
context['period_number'] = str(start_month).rjust(2, '0')
context['justify'] = justify
context['format_integer'] = format_integer
context['format_percentage'] = format_percentage
context['records_amount'] = sum(
(r.amount for r in records), Decimal(0))
context['justify'] = justify
context['identifier_code'] = identifier_code
def format_decimal(n, digits=13):
return ('{0:.2f}'.format(abs(n))).replace('.', '').rjust(
digits, '0')
context['format_decimal'] = format_decimal
return context
class ESVATBookContext(ModelView):
__name__ = 'account.reporting.vat_book_es.context'
company = fields.Many2One('company.company', "Company", required=True)
fiscalyear = fields.Many2One('account.fiscalyear', "Fiscal Year",
required=True,
domain=[
('company', '=', Eval('company', -1)),
])
start_period = fields.Many2One('account.period', "Start Period",
domain=[
('fiscalyear', '=', Eval('fiscalyear', -1)),
('start_date', '<=', (Eval('end_period'), 'start_date')),
])
end_period = fields.Many2One('account.period', "End Period",
domain=[
('fiscalyear', '=', Eval('fiscalyear', -1)),
('start_date', '>=', (Eval('start_period'), 'start_date'))
])
es_vat_book_type = fields.Selection([
# Use same key as tax authority
('E', "Issued"),
('R', "Received"),
('S', "Investment Goods"),
],
"Type", required=True)
@classmethod
def default_es_vat_book_type(cls):
return 'E'
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
def default_fiscalyear(cls):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
try:
fiscalyear = FiscalYear.find(
cls.default_company(), test_state=False)
except FiscalYearNotFoundError:
return None
return fiscalyear.id
class ESVATBook(ModelSQL, ModelView):
__name__ = 'account.reporting.vat_book_es'
invoice = fields.Many2One('account.invoice', "Invoice")
invoice_date = fields.Date("Invoice Date")
party = fields.Many2One('party.party', "Party")
party_tax_identifier = fields.Many2One(
'party.identifier', "Party Tax Identifier")
tax = fields.Many2One('account.tax', "Tax")
base_amount = Monetary(
"Base Amount", currency='currency', digits='currency')
tax_amount = Monetary(
"Tax Amount", currency='currency', digits='currency')
surcharge_tax = fields.Many2One('account.tax', "Surcharge Tax")
surcharge_tax_amount = Monetary(
"Surcharge Tax Amount", currency='currency', digits='currency',
states={
'invisible': ~(Eval('surcharge_tax', None)),
})
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"),
'get_currency')
@classmethod
def included_tax_groups(cls):
pool = Pool()
ModelData = pool.get('ir.model.data')
tax_groups = []
vat_book_type = Transaction().context.get('es_vat_book_type')
if vat_book_type == 'E':
tax_groups.append(ModelData.get_id(
'account_es', 'tax_group_sale'))
tax_groups.append(ModelData.get_id(
'account_es', 'tax_group_sale_service'))
elif vat_book_type == 'R':
tax_groups.append(ModelData.get_id(
'account_es', 'tax_group_purchase'))
tax_groups.append(ModelData.get_id(
'account_es', 'tax_group_purchase_service'))
elif vat_book_type == 'S':
tax_groups.append(ModelData.get_id(
'account_es', 'tax_group_purchase_investment'))
return tax_groups
@classmethod
def table_query(cls):
pool = Pool()
Company = pool.get('company.company')
Invoice = pool.get('account.invoice')
Move = pool.get('account.move')
Line = pool.get('account.move.line')
TaxLine = pool.get('account.tax.line')
Period = pool.get('account.period')
Tax = pool.get('account.tax')
context = Transaction().context
company = Company.__table__()
invoice = Invoice.__table__()
cancel_invoice = Invoice.__table__()
move = Move.__table__()
cancel_move = Move.__table__()
line = Line.__table__()
tax_line = TaxLine.__table__()
period = Period.__table__()
tax = Tax.__table__()
where = ((invoice.company == context.get('company'))
& (period.fiscalyear == context.get('fiscalyear'))
& ~tax.es_exclude_from_vat_book)
where &= ~Exists(cancel_invoice
.join(cancel_move,
condition=cancel_invoice.cancel_move == cancel_move.id)
.select(cancel_invoice.id, distinct=True,
where=((cancel_invoice.id == invoice.id)
& (~cancel_move.origin.like('account.invoice,%')))))
groups = cls.included_tax_groups()
if groups:
where &= tax.group.in_(groups)
if context.get('start_period'):
start_period = Period(context['start_period'])
where &= (period.start_date >= start_period.start_date)
if context.get('end_period'):
end_period = Period(context['end_period'])
where &= (period.end_date <= end_period.end_date)
query = (tax_line
.join(tax, condition=tax_line.tax == tax.id)
.join(line, condition=tax_line.move_line == line.id)
.join(move, condition=line.move == move.id)
.join(period, condition=move.period == period.id)
.join(invoice, condition=invoice.move == move.id)
.join(company, condition=company.id == invoice.company)
.select(
Min(tax_line.id).as_('id'),
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'),
cls.write_date.sql_cast(Literal(Null)).as_('write_date'),
invoice.id.as_('invoice'),
invoice.invoice_date.as_('invoice_date'),
invoice.party.as_('party'),
invoice.party_tax_identifier.as_('party_tax_identifier'),
Coalesce(tax.es_reported_with, tax.id).as_('tax'),
Sum(tax_line.amount,
filter_=((tax_line.type == 'base')
& (tax.es_reported_with == Null))).as_('base_amount'),
Coalesce(
Sum(tax_line.amount,
filter_=((tax_line.type == 'tax')
& (tax.es_reported_with == Null))),
0).as_('tax_amount'),
Min(tax.id,
filter_=(tax.es_reported_with != Null)).as_(
'surcharge_tax'),
Coalesce(Sum(tax_line.amount,
filter_=((tax_line.type == 'tax')
& (tax.es_reported_with != Null))), 0).as_(
'surcharge_tax_amount'),
where=where,
group_by=[
invoice.id,
invoice.party,
invoice.invoice_date,
invoice.party_tax_identifier,
Coalesce(tax.es_reported_with, tax.id),
]))
return query
def get_currency(self, name):
return self.invoice.company.currency.id
class VATBookReport(Report):
__name__ = 'account.reporting.aeat.vat_book'
@classmethod
def get_context(cls, records, header, data):
context = super().get_context(records, header, data)
context['format_decimal'] = cls.format_decimal
context['get_period'] = cls.get_period
return context
@classmethod
def render(cls, report, report_context):
return cls.render_csv(report, report_context)
@classmethod
def convert(cls, report, data, **kwargs):
output_format = report.extension or report.template_extension
if not report.report_content and output_format == 'csv':
return output_format, data
return super().convert(report, data, **kwargs)
@classmethod
def get_period(cls, date):
return str((date.month + 2) // 3) + 'T'
@classmethod
def format_decimal(cls, n):
if n is None:
return ''
sign = '-' if n < 0 else ''
return sign + '{0:.2f}'.format(abs(n)).replace('.', ',')
@classmethod
def get_format_date(cls):
pool = Pool()
Lang = pool.get('ir.lang')
es = Lang(code='es', date='%d/%m/%Y')
return lambda value: es.strftime(value, '%d/%m/%Y')
@classmethod
def render_csv(cls, report, report_context):
vat_book = BytesIO()
writer = csv.writer(
TextIOWrapper(vat_book, encoding='utf-8', write_through=True),
delimiter=';', doublequote=False, escapechar='\\',
quoting=csv.QUOTE_NONE)
for record in report_context['records']:
writer.writerow(cls.get_row(record, report_context))
return vat_book.getvalue()
@classmethod
def get_row(cls, record, report_context):
context = Transaction().context
format_date = cls.get_format_date()
return [
record.invoice_date.year,
report_context['get_period'](record.invoice_date),
context['es_vat_book_type'],
'',
record.invoice.es_vat_book_type,
'',
'',
format_date(record.invoice_date),
'',
record.invoice.es_vat_book_serie,
record.invoice.es_vat_book_number,
(record.party_tax_identifier.es_vat_type()
if record.party_tax_identifier else ''),
(record.party_tax_identifier.es_code()
if record.party_tax_identifier else ''),
country_code(record),
record.party.name[:40] if record.party.name else '',
'',
cls.format_decimal(record.invoice.total_amount),
cls.format_decimal(record.base_amount),
(cls.format_decimal(record.tax.rate * 100)
if record.tax.rate is not None else ''),
cls.format_decimal(record.tax_amount),
(cls.format_decimal(record.surcharge_tax.rate * 100)
if record.surcharge_tax is not None else ''),
(cls.format_decimal(record.surcharge_tax_amount)
if record.surcharge_tax else ''),
'',
'',
'',
'',
'',
'',
]