Files
tryton/modules/account_es/reporting_tax.py
T

992 lines
36 KiB
Python
Raw Normal View History

2018-09-01 14:55:15 +02:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2021-04-12 09:26:14 +02:00
import csv
2019-04-14 14:21:07 +02:00
import unicodedata
2018-09-01 14:55:15 +02:00
from collections import defaultdict
from decimal import Decimal
2021-12-11 17:58:41 +01:00
from io import BytesIO, TextIOWrapper
2018-09-01 14:55:15 +02:00
from operator import attrgetter
import phonenumbers
2021-12-11 17:58:41 +01:00
from dateutil.relativedelta import relativedelta
# XXX fix: https://genshi.edgewall.org/ticket/582
from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
from phonenumbers import NumberParseException
from sql import Literal, Null
from sql.aggregate import Count, Min, Sum
2021-04-12 09:26:14 +02:00
from sql.conditionals import Case, Coalesce
from sql.functions import CurrentTimestamp, Extract
from sql.operators import Exists
2018-09-01 14:55:15 +02:00
2018-12-29 14:20:28 +01:00
from trytond.i18n import gettext
2023-03-23 00:33:22 +01:00
from trytond.model import ModelSQL, ModelView, convert_from, fields
from trytond.modules.account.exceptions import FiscalYearNotFoundError
2021-12-11 17:58:41 +01:00
from trytond.modules.account_eu.account import ECSalesList, ECSalesListContext
from trytond.modules.currency.fields import Monetary
2018-09-01 14:55:15 +02:00
from trytond.pool import Pool
from trytond.pyson import Eval, If
2018-09-01 14:55:15 +02:00
from trytond.report import Report
from trytond.transaction import Transaction
2021-12-11 17:58:41 +01:00
from trytond.wizard import (
Button, StateReport, StateTransition, StateView, Wizard)
2018-09-01 14:55:15 +02:00
2018-12-29 14:20:28 +01:00
from .exceptions import PrintError
2018-09-01 16:47:34 +02:00
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
2018-09-01 14:55:15 +02:00
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')
def format_phone(phone):
try:
phone = phonenumbers.format_number(
phonenumbers.parse(phone, 'ES'),
phonenumbers.PhoneNumberFormat.NATIONAL)
except NumberParseException:
phone = ''
phone = phone.replace(' ', '')
return phone[:9].rjust(9, '0')
2019-04-14 14:21:07 +02:00
def identifier_code(identifier):
if identifier:
return identifier.es_code()
2019-04-14 14:21:07 +02:00
return ''
def country_code(record):
code = None
if record.party_tax_identifier:
code = record.party_tax_identifier.es_country()
2019-04-14 14:21:07 +02:00
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')
2018-09-01 14:55:15 +02:00
class AEATReport(Report):
@classmethod
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
context = super().get_context(records, header, data)
2018-09-01 14:55:15 +02:00
2021-02-21 19:19:26 +01:00
periods = sorted(records, key=attrgetter('start_date'))
2018-09-01 14:55:15 +02:00
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
2021-02-21 19:19:26 +01:00
with Transaction().set_context(periods=data['ids']):
2018-09-01 14:55:15 +02:00
context['amounts'] = cls.compute_amounts()
return context
@classmethod
def compute_amounts(cls):
2018-10-31 18:17:35 +01:00
amounts = defaultdict(Decimal)
2018-09-01 14:55:15 +02:00
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')
2018-09-01 14:55:15 +02:00
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)))),
2018-09-01 14:55:15 +02:00
}
return Case((is_invoice, invoice.party), else_=Null), tables
@classmethod
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
2018-09-01 14:55:15 +02:00
pool = Pool()
Move = pool.get('account.move')
Line = pool.get('account.move.line')
TaxLine = pool.get('account.tax.line')
Tax = pool.get('account.tax')
2021-02-21 19:19:26 +01:00
context = super().get_context(records, header, data)
2018-09-01 14:55:15 +02:00
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)
2021-02-21 19:19:26 +01:00
with Transaction().set_context(periods=data['ids']):
2018-09-01 14:55:15 +02:00
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
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
context = super().get_context(records, header, data)
2018-09-01 14:55:15 +02:00
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
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
context = super().get_context(records, header, data)
2018-09-01 14:55:15 +02:00
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
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
2018-09-01 14:55:15 +02:00
pool = Pool()
Account = pool.get('account.account')
2018-09-01 14:55:15 +02:00
TaxCodeLine = pool.get('account.tax.code.line')
transaction = Transaction()
2021-02-21 19:19:26 +01:00
context = super().get_context(records, header, data)
2018-09-01 14:55:15 +02:00
amounts = context['amounts']
2021-02-21 19:19:26 +01:00
periods = records
2018-09-01 14:55:15 +02:00
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']:
2018-09-01 14:55:15 +02:00
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)
2021-04-19 16:39:09 +02:00
amounts['110'] = amounts['78'] = amount_to_compensate
amounts['87'] = amounts['110'] - amounts['78']
amounts['69'] = (amounts['66'] + amounts['77'] - amounts['78']
2018-09-01 14:55:15 +02:00
+ 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')
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):
2021-02-21 19:19:26 +01:00
return action, {'ids': [p.id for p in self.start.periods]}
2018-09-01 14:55:15 +02:00
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:
2018-12-29 14:20:28 +01:00
raise PrintError(
gettext('account_es.msg_report_same_fiscalyear'))
2019-04-14 14:21:07 +02:00
class ESVATList(ModelSQL, ModelView):
"Spanish VAT List"
__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")
2021-08-21 09:09:55 +02:00
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')
2019-04-14 14:21:07 +02:00
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']
2019-04-14 14:21:07 +02:00
@classmethod
def table_query(cls):
pool = Pool()
Company = pool.get('company.company')
2019-04-14 14:21:07 +02:00
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')
2019-04-14 14:21:07 +02:00
Tax = pool.get('account.tax')
TaxCode = pool.get('account.tax.code')
TaxCodeLine = pool.get('account.tax.code.line')
2019-04-14 14:21:07 +02:00
Date = pool.get('ir.date')
context = Transaction().context
company = Company.__table__()
2019-04-14 14:21:07 +02:00
invoice = Invoice.__table__()
cancel_invoice = Invoice.__table__()
move = Move.__table__()
cancel_move = Move.__table__()
line = Line.__table__()
tax_line = TaxLine.__table__()
2019-04-14 14:21:07 +02:00
tax = Tax.__table__()
tax_code = TaxCode.__table__()
tax_code_line = TaxCodeLine.__table__()
exclude_invoice_tax = InvoiceTax.__table__()
2019-04-14 14:21:07 +02:00
amount = tax_line.amount
2019-04-14 14:21:07 +02:00
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())))
2019-04-14 14:21:07 +02:00
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))))))
return (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)
2019-04-14 14:21:07 +02:00
.select(
Min(tax_line.id).as_('id'),
2019-04-14 14:21:07 +02:00
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'),
2019-04-14 14:21:07 +02:00
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'),
2019-04-14 14:21:07 +02:00
where=where,
group_by=[
invoice.tax_identifier,
invoice.type,
invoice.party,
invoice.party_tax_identifier,
company.currency,
2019-04-14 14:21:07 +02:00
tax.es_vat_list_code,
]))
class ESVATListContext(ModelView):
"Spanish VAT List Context"
__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
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
2019-04-14 14:21:07 +02:00
pool = Pool()
Company = pool.get('company.company')
t_context = Transaction().context
2021-02-21 19:19:26 +01:00
context = super().get_context(records, header, data)
2019-04-14 14:21:07 +02:00
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):
if not isinstance(n, Decimal):
n = 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['format_phone'] = format_phone
2019-04-14 14:21:07 +02:00
context['identifier_code'] = identifier_code
context['country_code'] = country_code
context['strip_accents'] = strip_accents
return context
class ECOperationList(ECSalesList):
"EC Operation List"
__name__ = 'account.reporting.es_ec_operation_list'
@classmethod
def table_query(cls):
pool = Pool()
Company = pool.get('company.company')
2019-04-14 14:21:07 +02:00
Invoice = pool.get('account.invoice')
Move = pool.get('account.move')
Line = pool.get('account.move.line')
TaxLine = pool.get('account.tax.line')
2019-04-14 14:21:07 +02:00
Period = pool.get('account.period')
Tax = pool.get('account.tax')
context = Transaction().context
company = Company.__table__()
2019-04-14 14:21:07 +02:00
invoice = Invoice.__table__()
cancel_invoice = Invoice.__table__()
2019-04-14 14:21:07 +02:00
move = Move.__table__()
cancel_move = Move.__table__()
line = Line.__table__()
tax_line = TaxLine.__table__()
2019-04-14 14:21:07 +02:00
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'))
2019-04-14 14:21:07 +02:00
where &= ((tax.es_ec_purchases_list_code != Null)
& (tax.es_ec_purchases_list_code != ''))
where &= tax_line.type == 'base'
2019-04-14 14:21:07 +02:00
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)
2019-04-14 14:21:07 +02:00
.join(period, condition=move.period == period.id)
.join(invoice, condition=invoice.move == move.id)
.join(company, condition=company.id == invoice.company)
2019-04-14 14:21:07 +02:00
.select(
Min(tax_line.id).as_('id'),
2019-04-14 14:21:07 +02:00
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'),
2019-04-14 14:21:07 +02:00
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'),
2019-04-14 14:21:07 +02:00
where=where,
group_by=[
invoice.tax_identifier,
invoice.party,
invoice.party_tax_identifier,
tax.es_ec_purchases_list_code,
company.currency,
2019-04-14 14:21:07 +02:00
]))
return sales | purchases
class ECOperationListContext(ECSalesListContext):
"EC Operation List Context"
__name__ = 'account.reporting.es_ec_operation_list.context'
start_date = fields.Date("Start Date",
domain=[
If(Eval('end_date'),
('start_date', '<=', Eval('end_date')),
(),
),
])
end_date = fields.Date("End Date",
domain=[
If(Eval('start_date'),
('end_date', '>=', Eval('start_date')),
(),
),
])
@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)
2019-04-14 14:21:07 +02:00
class AEAT349(Report):
__name__ = 'account.reporting.aeat349'
@classmethod
2021-02-21 19:19:26 +01:00
def get_context(cls, records, header, data):
2019-04-14 14:21:07 +02:00
pool = Pool()
Company = pool.get('company.company')
2019-04-14 14:21:07 +02:00
t_context = Transaction().context
2021-02-21 19:19:26 +01:00
context = super().get_context(records, header, data)
2019-04-14 14:21:07 +02:00
context['company'] = Company(t_context['company'])
2019-04-14 14:21:07 +02:00
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
2019-04-14 14:21:07 +02:00
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):
if not isinstance(n, Decimal):
n = Decimal(n)
return ('{0:.2f}'.format(abs(n))).replace('.', '').rjust(
digits, '0')
context['format_decimal'] = format_decimal
context['format_phone'] = format_phone
2019-04-14 14:21:07 +02:00
return context
2021-04-12 09:26:14 +02:00
class ESVATBookContext(ModelView):
"Spanish VAT Book Context"
__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')),
])
2021-04-12 09:26:14 +02:00
start_period = fields.Many2One('account.period', "Start Period",
domain=[
('fiscalyear', '=', Eval('fiscalyear')),
('start_date', '<=', (Eval('end_period'), 'start_date')),
])
2021-04-12 09:26:14 +02:00
end_period = fields.Many2One('account.period', "End Period",
domain=[
('fiscalyear', '=', Eval('fiscalyear')),
('start_date', '>=', (Eval('start_period'), 'start_date'))
])
2021-04-12 09:26:14 +02:00
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
2021-04-12 09:26:14 +02:00
class ESVATBook(ModelSQL, ModelView):
"Spanish VAT Book"
__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")
2021-08-21 09:09:55 +02:00
base_amount = Monetary(
"Base Amount", currency='currency', digits='currency')
tax_amount = Monetary(
"Tax Amount", currency='currency', digits='currency')
2021-04-12 09:26:14 +02:00
surcharge_tax = fields.Many2One('account.tax', "Surcharge Tax")
2021-08-21 09:09:55 +02:00
surcharge_tax_amount = Monetary(
"Surcharge Tax Amount", currency='currency', digits='currency',
states={
'invisible': ~(Eval('surcharge_tax', None)),
})
2021-08-21 09:09:55 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"),
'get_currency')
2021-04-12 09:26:14 +02:00
@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__()
2021-04-12 09:26:14 +02:00
move = Move.__table__()
cancel_move = Move.__table__()
2021-04-12 09:26:14 +02:00
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,%')))))
2021-04-12 09:26:14 +02:00
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_(
2021-04-12 09:26:14 +02:00
'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
2021-08-21 09:09:55 +02:00
def get_currency(self, name):
return self.invoice.company.currency.id
2021-04-12 09:26:14 +02:00
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 ''
2021-04-12 09:26:14 +02:00
if not isinstance(n, Decimal):
n = Decimal(n)
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()
2021-04-12 09:26:14 +02:00
writer = csv.writer(
TextIOWrapper(vat_book, encoding='utf-8', write_through=True),
delimiter=';', doublequote=False, escapechar='\\',
2021-04-12 09:26:14 +02:00
quoting=csv.QUOTE_NONE)
for record in report_context['records']:
writer.writerow(cls.get_row(record, report_context))
return vat_book.getvalue()
2021-04-12 09:26:14 +02:00
@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 '',
2021-04-12 09:26:14 +02:00
'',
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 ''),
2021-04-12 09:26:14 +02:00
cls.format_decimal(record.tax_amount),
(cls.format_decimal(record.surcharge_tax.rate * 100)
if record.surcharge_tax is not None else ''),
2021-04-12 09:26:14 +02:00
(cls.format_decimal(record.surcharge_tax_amount)
if record.surcharge_tax else ''),
'',
'',
'',
'',
'',
'',
]