Files
enterprise-addons/l10n_es_reports/models/aeat_tax_reports.py
T

1982 lines
122 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import RedirectWarning, UserError
import odoo.release
from odoo.tools import SQL, float_is_zero
from odoo.tools.float_utils import float_split_str, float_compare
from datetime import datetime
from dateutil.relativedelta import relativedelta
import re
import unicodedata
SPANISH_PROVINCES_REPORT_CODES = {
'VI': '01',
'AB': '02',
'A': '03',
'AL': '04',
'AV': '05',
'BA': '06',
'PM': '07',
'B': '08',
'BU': '09',
'CC': '10',
'CA': '11',
'CS': '12',
'CR': '13',
'CO': '14',
'C': '15',
'CU': '16',
'GI': '17',
'GR': '18',
'GU': '19',
'SS': '20',
'H': '21',
'HU': '22',
'J': '23',
'LE': '24',
'L': '25',
'LO': '26',
'LU': '27',
'M': '28',
'MA': '29',
'MU': '30',
'NA': '31',
'OR': '32',
'O': '33',
'P': '34',
'GC': '35',
'PO': '36',
'SA': '37',
'TF': '38',
'S': '39',
'SG': '40',
'SE': '41',
'SO': '42',
'T': '43',
'TE': '44',
'TO': '45',
'V': '46',
'VA': '47',
'BI': '48',
'ZA': '49',
'Z': '50',
'CE': '51',
'ME': '52',
}
# including retention tax lines as we need to get the amounts before retention
MOD_347_CUSTOM_ENGINES_DOMAINS = {
'_report_custom_engine_threshold_insurance_bought': [
('move_id.l10n_es_reports_mod347_invoice_type', '=', 'insurance'),
('move_id.move_type', 'in', ('in_invoice', 'in_refund', 'in_receipt')),
'|',
('account_type', '=', 'liability_payable'),
('tax_line_id.l10n_es_type', '=', 'retencion'),
],
'_report_custom_engine_threshold_insurance_sold': [
('move_id.l10n_es_reports_mod347_invoice_type', '=', 'insurance'),
('move_id.move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt')),
'|',
('account_type', '=', 'asset_receivable'),
('tax_line_id.l10n_es_type', '=', 'retencion'),
],
'_report_custom_engine_threshold_regular_bought': [
('move_id.l10n_es_reports_mod347_invoice_type', '=', 'regular'),
('move_id.move_type', 'in', ('in_invoice', 'in_refund', 'in_receipt')),
'|',
('account_type', '=', 'liability_payable'),
('tax_line_id.l10n_es_type', '=', 'retencion'),
],
'_report_custom_engine_threshold_regular_sold': [
('move_id.l10n_es_reports_mod347_invoice_type', '=', 'regular'),
('move_id.move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt')),
'|',
('account_type', '=', 'asset_receivable'),
('tax_line_id.l10n_es_type', '=', 'retencion'),
],
'_report_custom_engine_threshold_all_operations': [
('move_id.l10n_es_reports_mod347_invoice_type', '!=', None),
'|',
('account_type', 'in', ('asset_receivable', 'liability_payable')),
('tax_line_id.l10n_es_type', '=', 'retencion'),
],
}
MOD_349_KEYS = ('A', 'E', 'T', 'S', 'I', 'M', 'H', 'R', 'D', 'C')
MOD_349_CUSTOM_ENGINES_SPLIT_REGEX = re.compile(r'_report_custom_engine_modelo349_(invoice|refund)_([eatsimhrdc])')
class AccountReport(models.Model):
_inherit = 'account.report'
def _get_expression_audit_aml_domain(self, expression, options):
if expression.engine == 'custom':
# Allow auditing mod347's threshold lines (for consistency: this way all the lines of the report are audited in the same way)
if expression.formula in MOD_347_CUSTOM_ENGINES_DOMAINS:
return MOD_347_CUSTOM_ENGINES_DOMAINS[expression.formula]
# Allow auditing mod349 summary lines
matching = MOD_349_CUSTOM_ENGINES_SPLIT_REGEX.fullmatch(expression.formula)
if matching:
move_type = matching.group(1)
key = matching.group(2).capitalize()
return self.env[self.custom_handler_model_name]._get_modelo349_audit_aml_domain(options, key, move_type)
return super()._get_expression_audit_aml_domain(expression, options)
class L10n_EsTaxReportHandler(models.AbstractModel):
_name = 'l10n_es.tax.report.handler'
_inherit = ['account.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler'
def _append_boe_button(self, options, boe_number):
options['buttons'].append(
{
'name': _('BOE'),
'sequence': 0,
'action': 'open_boe_wizard',
'action_param': boe_number,
'file_export_type': _('BOE'),
'branch_allowed': True,
})
def open_boe_wizard(self, options, boe_number):
""" Triggers the generation of the BOE file for the current mod report.
In case this BOE file needs some more data to be entered manually by
the user, it show instead a wizard prompting for them, which will, once
validated and closed, trigger the generation of the BOE itself.
"""
period, _year = self._get_mod_period_and_year(options)
if boe_number == 390:
# period will be falsy if a whole year is selected
if period and not options.get('_running_export_test'):
raise UserError(_("Wrong report dates for BOE generation : please select a range of a year."))
elif boe_number != 347 and not period:
raise UserError(_("Wrong report dates for BOE generation : please select a range of one month or a trimester."))
return {
'name': _('Print BOE'),
'view_mode': 'form',
'views': [(False, 'form')],
'res_model': f'l10n_es_reports.aeat.boe.mod{boe_number}.export.wizard',
'type': 'ir.actions.act_window',
'target': 'new',
'context': {
**self.env.context,
'l10n_es_reports_report_options': options,
'default_report_id': options['sections_source_id'],
},
}
def _get_mod_period_and_year(self, options):
""" Returns the period and year (in terms of AEAT modulo reports regulation)
corresponding to the report options given in parameters, in the form
of a tuple (period, year). Period will be None if the dates do not fit
any.
A UserError will be raised if the start and end date of given in the
options do not corresond to the first and last day of their respective
month, or belong to two different years.
"""
date_options = (options or {}).get('date', {})
if not date_options.get('date_from') or not date_options.get('date_to'):
raise UserError(_("Cannot generate a BOE file for an open-ended date range."))
date_from = datetime.strptime(options['date']['date_from'], "%Y-%m-%d")
date_to = datetime.strptime(options['date']['date_to'], "%Y-%m-%d")
if not date_from.year == date_to.year:
raise UserError(_("Cannot generate a BOE file for two different years"))
if date_from.day != 1 or date_to.day != (date_to + relativedelta(day=31)).day:
raise UserError(_("Your date range does not cover entire months, please use a start and end date matching respectively the first and last day of a month."))
rslt_period = None
rslt_year = str(date_from.year) # Identical to date_to.year thanks to the previous conditions
if date_from.month == date_to.month:
rslt_period = '%02d' % (date_from.month)
elif date_from.month == date_to.month - 2 and self._retrieve_period_and_year(date_from, trimester=True)[0] == self._retrieve_period_and_year(date_to, trimester=True)[0]:
rslt_period = '%01dT' % (date_to.month / 3)
# Period stays None otherwize, so we can use rslt_period == None to check if a trimester or year is selected
return rslt_period, rslt_year
def _retrieve_period_and_year(self, date, trimester=False):
""" Retrieves the period and year (in the form of a tuple) corresponding
to a given date.
:param trimester: whether or not we use trimesters as periods.
"""
if trimester:
return '%01dT' % (1 + ((date.month - 1) // 3)), str(date.year)
else:
return '%02d' % date.month, str(date.year)
def _convert_period_to_dates(self, period, year):
""" Converts a period and a year to a tuple of two dates, respectively its
start and end date.
"""
if period[-1] == 'T':
quarter = int(period[:-1])
return datetime(day=1, month=1 + (quarter - 1) * 3, year=int(year)), (datetime(day=1, month=quarter * 3, year=int(year)) + relativedelta(day=31)) # relativedelta used to force last day of the month without triggering ValueError for months with less than 31 days
else:
return datetime(day=1, month=int(period), year=int(year)), datetime(day=1, month=int(period), year=int(year)) + relativedelta(day=31)
def _l10n_es_boe_format_string(self, string, length=-1, align='left', fill_char=b' '):
""" Formats a string so that it is BOE-compatible.
:param string: the string to format
:param length: the desired length of the resulting string, or -1 if there is not
:param align: 'left' or 'right', depending on the side of the result string where string must placed (no effect if no length is given)
:param fill_char: the character that will be used to bring the result string to a size of length (no effect if length is not specified)
"""
string = string and string.upper() or ''
rslt = b''
for char in unicodedata.normalize('NFKC', string): # We use a normalized version of the string here so that we are sure accentuated charcaters are each time encoded with only one character (and not a regular character followed by a combining one)
if not char in ('Ñ', 'Ç'):
normalized_char = unicodedata.normalize('NFD', char) # Not NFKD, as the NFKC normalization in the loop already ensures good treatment of compatibility characters. NFD splits accentuated characters in two parts: the original character, and the accent to combine it with
rslt += normalized_char.encode('ISO-8859-1', 'ignore') # Combinable accentuation characters are not supported by this encoding, and disappear when transcoding.
else:
rslt += char.encode('ISO-8859-1')
if length > -1:
rslt = rslt[:length]
if align == 'left':
rslt = rslt.ljust(length, fill_char)
elif align == 'right':
rslt = rslt.rjust(length, fill_char)
return rslt
def _l10n_es_boe_format_number(self, options, number, length=-1, decimal_places=0, signed=False, sign_neg='N', sign_pos='', in_currency=False):
""" Formats a number to a BOE-compatible string.
:param number: the number to format
:param length: the desired length for the resulting string, or -1, to just use the number of characters of the number.
:param decimal_places: the number of decimal places to use (these characters are part of the length limit)
:param signed: whether or not the number must be signed in the resulting string
:param sign_neg: the character to use as the first character of the resulting string if signed is True and
the number was negative (the resulting string will contain no additional - sign)
:param sign_pos: same as sign_neg, but if number is positive
:param in_currency: True iff number is expressed in company currency (and thus needs to be converted in €)
"""
company = self.env.company
if in_currency:
# If number is an amount expressed in company currency, we ensure that it
# is written in € in BOE file
conversion_date = options['date']['date_to']
curr_eur = self.env["res.currency"].search([('name', '=', 'EUR')], limit=1)
number = company.currency_id._convert(number, curr_eur, company, conversion_date)
if isinstance(number, float):
split_number = float_split_str(abs(number), decimal_places)
str_number = split_number[0] + split_number[1]
else:
str_number = str(abs(number)) + '0' * decimal_places
negative_amount = in_currency and company.currency_id.compare_amounts(number, 0.0) == -1 or number < 0
sign_str = signed and (negative_amount and sign_neg or sign_pos) or ''
# Done in two parts, so that sign str is always in front of the filling characters
return self._l10n_es_boe_format_string(sign_str) + self._l10n_es_boe_format_string(str_number, length=length - len(sign_str), align='right', fill_char=b'0')
def _retrieve_casilla_lines(self, report_lines):
""" Retrieves the values of the casillas contained in report_lines, using
the fact that these lines' names are prefixed by their number between [] to
identify them. Returns a dictionnary, with casillas as keys and their values
as values.
"""
casilla_pattern = re.compile(r'\[(?P<casilla>.*)\]')
rslt = {}
for line in report_lines:
matcher = casilla_pattern.match(line['name'])
if matcher:
casilla = matcher.group('casilla')
casilla_value = line['columns'][0]['no_format'] # Element [0] is the current period, in case we are comparing
rslt[casilla] = casilla_value
return rslt
def _retrieve_report_expression(self, options, xmlid):
""" Retrieves the data of the report line denoted by xmlid, with respect
to the given options.
"""
expression = self.env.ref(xmlid)
expression_totals = self.env['account.report'].browse(options['report_id'])._compute_expression_totals_for_each_column_group(expression._expand_aggregations(), options)
# This considers we have but one column group
return next(expr_total[expression]['value'] for expr_total in expression_totals.values())
def _get_bic_and_iban(self, res_partner_bank):
""" Convenience method returning (bic,iban) of the given account if
this account exists, or a tuple of empty strings otherwise.
"""
if res_partner_bank:
return res_partner_bank.bank_bic or "", res_partner_bank.sanitized_acc_number
return '', ''
def _retrieve_boe_manual_wizard(self, options, modelo_number):
""" Retrieves a BOE manual wizard object from its id, contained within the
options dict.
"""
return self.env[f'l10n_es_reports.aeat.boe.mod{modelo_number}.export.wizard'].browse(options['l10n_es_reports_boe_wizard_id'])
def _call_on_partner_sublines(self, report_options, line_xml_id, fun_to_call, required_ids_set=None):
""" Calls a function on the data of all the sublines generated by a
groupby parameter for a report line (except the one giving the total).
:param report_options: the options to use to generate line data
:param line_xml_id: the xml id of the report line whose children we want to call our function on
:param fun_to_call: the function to call on sublines. It must take only one argument, the data dictionary of the subline.
:param required_ids_set: a set containing ids on which we want fun_to_call to be called.
This is used to generate data for models that are not present
in the grouped line displayed on the report. (this can for example
happen if they have no operation in this year; but
some data to be added into BOE make in necessary to still include
them in the file). This set will be modified by the function.
"""
if required_ids_set is None:
required_ids_set = set()
rslt = self._l10n_es_boe_format_string('')
report = self.env['account.report'].browse(report_options['report_id'])
report_line = self.env.ref(line_xml_id)
line_dict_id = report._get_generic_line_id('account.report.line', report_line.id)
for subline in report._report_expand_unfoldable_line_with_groupby(line_dict_id, report_line.groupby, report_options, None, 0)['lines']:
subline_model, subline_model_id = report._get_model_info_from_id(subline['id'])
if subline_model == 'res.partner':
rslt += fun_to_call({'line_data': subline, 'line_xml_id': line_xml_id, 'report_options': report_options})
if subline_model_id in required_ids_set:
required_ids_set.remove(subline_model_id)
for element in required_ids_set: # These elements are the ones for wich no line was generated, but that were into the original required ids set. So, we still treat them.
rslt += fun_to_call({
'line_data': {'id': report._get_generic_line_id('res.partner', element)},
'line_xml_id': line_xml_id,
'report_options': report_options
})
return rslt
def _get_partner_subline(self, report_options, line_xml_id, partner_id):
""" Returns the data of a subline generated by a groupby parameter, if its
'id' (i.e. the actual id of the model denoted by groupby represented by the
line) is equal to a given value.
:param report_options: the options to use to generate data
:param line_xml_id: the xml id of the parent line
:param sub_line_id: the id of the "grouped by" model corresponding to the subline we want to retrieve
"""
report = self.env['account.report'].browse(report_options['report_id'])
report_line = self.env.ref(line_xml_id)
line_dict_id = report._get_generic_line_id('account.report.line', report_line.id)
for subline in report._report_expand_unfoldable_line_with_groupby(line_dict_id, report_line.groupby, report_options, None, 0)['lines']:
subline_model, model_id = report._get_model_info_from_id(subline['id'])
if subline_model == 'res.partner' and model_id == partner_id:
return subline
def _extract_tin(self, partner, error_if_no_tin=True):
if not partner.vat:
if error_if_no_tin:
raise UserError(_(
"No TIN set for partner %(name)s (id %(id)d). Please define one.",
name=partner.name,
id=partner.id,
))
else:
return ''
country_code, number = partner._split_vat(partner.vat)
return country_code.upper() + number
def _extract_spanish_tin(self, partner, except_if_foreign=False):
formatted_tin = self._extract_tin(partner, error_if_no_tin=True)
is_spanish = formatted_tin[:2] == 'ES' or not partner.country_id or partner.country_code == 'ES'
if is_spanish:
return formatted_tin.removeprefix('ES')
elif except_if_foreign:
raise UserError(_("Reading a non-Spanish TIN as a Spanish TIN."))
return ''
def _generate_111_115_common_header(self, options, period, year, modelo_number):
rslt = b''
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, modelo_number)
# Header
current_company = self.env.company
rslt += self._l10n_es_boe_format_string(f"<T{modelo_number}0{year}{period}0000>")
rslt += self._l10n_es_boe_format_string('<AUX>')
rslt += self._l10n_es_boe_format_string(' ' * 70) # Reserved for AEAT
odoo_version = odoo.release.version.split('.')
rslt += self._l10n_es_boe_format_string(str(odoo_version[0]) + str(odoo_version[1]), length=4)
rslt += self._l10n_es_boe_format_string(' ' * 4) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(' ' * 213) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string('</AUX>')
# Fills in the common fields between mod 111 and 115
rslt += self._l10n_es_boe_format_string(f"<T{modelo_number}01000>")
rslt += self._l10n_es_boe_format_string(' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.declaration_type)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(current_company.name, length=60)
rslt += self._l10n_es_boe_format_string(' ' * 20) # We keep the name of the declaring party blank here, as it is a company
rslt += self._l10n_es_boe_format_string(year)
rslt += self._l10n_es_boe_format_string(period)
return rslt
class L10n_EsMod111TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod111.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod111)'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 111)
def export_boe(self, options):
period, year = self._get_mod_period_and_year(options)
rslt = self._generate_111_115_common_header(options, period, year, 111)
report = self.env['account.report'].browse(options['report_id'])
report_lines = report._get_lines(options)
casilla_lines_map = self._retrieve_casilla_lines(report_lines)
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 111)
# Content of the report
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['01'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['02'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['03'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['04'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['05'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['06'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['07'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['08'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['09'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['10'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['11'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['12'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['13'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['14'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['15'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['16'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['17'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['18'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['19'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['20'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['21'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['22'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['23'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['24'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['25'], length=8, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['26'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['27'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['28'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['29'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['30'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and boe_wizard.previous_report_number or '', length=13)
rslt += self._l10n_es_boe_format_string(' ') # Reserved for AEAT
dummy, iban = self._get_bic_and_iban(boe_wizard.partner_bank_id)
rslt += self._l10n_es_boe_format_string(iban, length=34)
rslt += self._l10n_es_boe_format_string(' ' * 389) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 13) # Reserved for AEAT
# We close the tags... (They have been opened by _generate_111_115_common_header)
rslt += self._l10n_es_boe_format_string('</T11101000>')
rslt += self._l10n_es_boe_format_string('</T1110' + year + period + '0000>')
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': rslt,
'file_type': 'txt',
}
class L10n_EsMod115TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod115.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod115)'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 115)
def export_boe(self, options):
period, year = self._get_mod_period_and_year(options)
rslt = self._generate_111_115_common_header(options, period, year, 115)
report = self.env['account.report'].browse(options['report_id'])
report_lines = report._get_lines(options)
casilla_lines_map = self._retrieve_casilla_lines(report_lines)
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 115)
# Content of the report
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['01'], length=15, signed=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['02'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['03'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['04'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['05'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and boe_wizard.previous_report_number or '', length=13)
dummy, iban = self._get_bic_and_iban(boe_wizard.partner_bank_id)
rslt += self._l10n_es_boe_format_string(iban, length=34)
rslt += self._l10n_es_boe_format_string(' ' * 236) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 13) # Reserved for AEAT
# We close the tags... (They have been opened by _generate_111_115_common_header)
rslt += self._l10n_es_boe_format_string('</T11501000>')
rslt += self._l10n_es_boe_format_string('</T1150' + year + period + '0000>')
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': rslt,
'file_type': 'txt',
}
class L10n_EsMod130TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod130.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod130)'
def _custom_options_initializer(self, report, options, previous_options=None):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 130)
def open_boe_wizard(self, options, boe_number):
result = super().open_boe_wizard(options, boe_number)
result['res_model'] = 'l10n_es_reports.aeat.boe.mod130.export.wizard'
return result
def export_boe(self, options):
period, year = self._get_mod_period_and_year(options)
# Legal requirement for the export of boe file for modelo 130
boe_modelo_id = 'T13001000'
rslt = f"<{boe_modelo_id}>".encode()
report = self.env['account.report'].browse(options['report_id'])
report_lines = report._get_lines(options)
casilla_lines_map = self._retrieve_casilla_lines(report_lines)
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 130)
rslt += self._l10n_es_boe_format_string(' ' * 1)
rslt += self._l10n_es_boe_format_string(f'{boe_wizard.declaration_type}')
rslt += self._l10n_es_boe_format_string(boe_wizard.taxpayer_id or 'n/a', length=9)
rslt += self._l10n_es_boe_format_string(boe_wizard.taxpayer_last_name or 'n/a', length=60)
rslt += self._l10n_es_boe_format_string(boe_wizard.taxpayer_first_name or 'n/a', length=20)
rslt += self._l10n_es_boe_format_string(year, length=4)
rslt += self._l10n_es_boe_format_string(period, length=2)
# Content of the report
for casilla in casilla_lines_map.values():
rslt += self._l10n_es_boe_format_number(options, casilla, length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and boe_wizard.previous_report_number or '', length=13)
_, iban = self._get_bic_and_iban(boe_wizard.partner_bank_id)
rslt += self._l10n_es_boe_format_string(iban, length=34)
rslt += self._l10n_es_boe_format_string(' ' * 96)
rslt += self._l10n_es_boe_format_string(' ' * 13)
rslt += self._l10n_es_boe_format_string(f'</{boe_modelo_id}>')
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': rslt,
'file_type': 'txt',
}
class L10n_EsMod303TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod303.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod303)'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 303)
def export_boe(self, options):
period, year = self._get_mod_period_and_year(options)
report = self.env['account.report'].browse(options['report_id'])
report_lines = report._get_lines(options)
casilla_lines_map = self._retrieve_casilla_lines(report_lines)
current_company = self.env.company
# Header
rslt = self._l10n_es_boe_format_string('<T3030' + year + period + '0000>')
rslt += self._l10n_es_boe_format_string('<AUX>')
rslt += self._l10n_es_boe_format_string(' ' * 70)
odoo_version = odoo.release.version.split('.')
rslt += self._l10n_es_boe_format_string(str(odoo_version[0]) + str(odoo_version[1]), length=4)
rslt += self._l10n_es_boe_format_string(' ' * 4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(' ' * 213)
rslt += self._l10n_es_boe_format_string('</AUX>')
rslt += self._generate_page1(report, options, current_company, period, year, casilla_lines_map)
rslt += self._generate_page3(report, options, current_company, period, year, casilla_lines_map)
if options['date']['date_from'] >= '2023-01-01':
rslt += self._generate_page_did(report, options, current_company, period, year, casilla_lines_map)
# We don't need page 2 and 4 (specified in AEAT doc)
# We close the tags...
rslt += self._l10n_es_boe_format_string('</T3030' + year + period + '0000>')
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': rslt,
'file_type': 'txt',
}
def _generate_page1(self, report, options, current_company, period, year, casilla_lines_map):
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 303)
rslt = self._l10n_es_boe_format_string('<T30301000>')
rslt += self._l10n_es_boe_format_string(' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.declaration_type)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(current_company.name, length=80)
rslt += self._l10n_es_boe_format_string(year)
rslt += self._l10n_es_boe_format_string(period)
# Identification
rslt += self._l10n_es_boe_format_number(options, 2) # Tributación exclusivamente foral => Always "no", for simplicity
rslt += self._l10n_es_boe_format_number(options, boe_wizard.monthly_return and 1 or 2)
rslt += self._l10n_es_boe_format_number(options, 3)
rslt += self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_string(' ' * 8)
rslt += self._l10n_es_boe_format_string(' ')
rslt += self._l10n_es_boe_format_number(options, boe_wizard._get_using_sii_2021_value())
exonerated_from_mod_390 = boe_wizard._get_exonerated_from_mod_390_2021_value(period)
rslt += self._l10n_es_boe_format_number(options, exonerated_from_mod_390)
if exonerated_from_mod_390 == 1:
profit_and_loss_report = self.env.ref('l10n_es_reports.financial_report_es_profit_and_loss')
end_date = fields.Date.from_string(options['date']['date_to'])
transactions_volume_options = profit_and_loss_report.get_options({
'date': {
'date_from': '%s-01-01' % end_date.year,
'date_to': '%s-12-31' % end_date.year,
'filter': 'custom',
'mode': 'range',
},
})
transactions_volume = self._retrieve_report_expression(transactions_volume_options, 'l10n_es_reports.es_profit_and_loss_line_1_balance')
annual_volume_indicator = current_company.currency_id.is_zero(transactions_volume) and 2 or 1
else:
annual_volume_indicator = 0
rslt += self._l10n_es_boe_format_number(options, annual_volume_indicator)
if int(year) >= 2026:
number = period not in ('01', '1T', '2T', '3T', '4T') and 2 or 0
rslt += self._l10n_es_boe_format_number(options, number, length=1)
# Casillas
if options['date']['date_from'] >= '2023-01-01':
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('150', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 0, length=5) # Casilla 151 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('152', 0), length=17, decimal_places=2, in_currency=True)
if int(year) >= 2026:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('165', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 0, length=5) # 166
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('167', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['01'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 400, length=5) # Casilla 02 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['03'], length=17, decimal_places=2, in_currency=True)
if options['date']['date_from'] >= '2023-01-01':
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('153', 0), length=17, decimal_places=2, in_currency=True)
if options['date']['date_from'] >= '2025-01-01':
casilla_154 = 0
else:
casilla_154 = 750 if options['date']['date_from'] >= '2024-10-01' else 500
rslt += self._l10n_es_boe_format_number(options, casilla_154, length=5)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('155', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['04'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 1000, length=5) # Casilla 05 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['06'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['07'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 2100, length=5) # Casilla 08 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['09'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['10'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['11'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['12'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['13'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['14'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['15'], length=17, decimal_places=2, signed=True, in_currency=True)
if options['date']['date_from'] >= '2023-01-01':
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('156', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 175, length=5) # Casilla 157 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('158', 0), length=17, decimal_places=2, in_currency=True)
if int(year) >= 2026:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('168', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 50, length=5)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('170', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['16'], length=17, decimal_places=2, in_currency=True)
if options['date']['date_from'] >= '2025-01-01':
casilla_17 = 0
else:
casilla_17 = 100 if options['date']['date_from'] >= '2024-10-01' else 50
rslt += self._l10n_es_boe_format_number(options, casilla_17, length=5)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['18'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['19'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 140, length=5) # Casilla 20 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['21'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['22'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 520, length=5) # Casilla 23 is constant
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['24'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['25'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['26'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['27'], length=17, decimal_places=2, signed=True, in_currency=True)
for casilla in range(28, 40):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, in_currency=True)
for casilla in range(40, 47):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
reserved_empty_chars = 600
if int(year) < 2026:
if options['date']['date_from'] >= '2024-10-01':
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('165', 0), length=17, decimal_places=2, in_currency=True)
if options['date']['date_from'] >= '2025-01-01':
rslt += self._l10n_es_boe_format_number(options, 0, length=5)
else:
rslt += self._l10n_es_boe_format_number(options, 200, length=5)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('167', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('168', 0), length=17, decimal_places=2, in_currency=True)
if options['date']['date_from'] >= '2025-01-01':
rslt += self._l10n_es_boe_format_number(options, 50, length=5)
else:
rslt += self._l10n_es_boe_format_number(options, 26, length=5)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('170', 0), length=17, decimal_places=2, in_currency=True)
reserved_empty_chars = 522
elif options['date']['date_to'] >= '2024-09-30':
rslt += self._l10n_es_boe_format_number(options, 0, length=78)
reserved_empty_chars = 522
else:
reserved_empty_chars = 521
# Footer of page 1
rslt += self._l10n_es_boe_format_string(' ' * reserved_empty_chars) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 13) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string('</T30301000>')
return rslt
def _generate_page3(self, report, options, current_company, period, year, casilla_lines_map):
rslt = self._l10n_es_boe_format_string('<T30303000>')
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 303)
boe_wizard_fields = boe_wizard.fields_get()
# Casillas
to_treat = ['59', '60']
for casilla in to_treat:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[casilla], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['120'], length=17, decimal_places=2, signed=True, in_currency=True)
if options['date']['date_from'] < '2022-01-01':
rslt += self._l10n_es_boe_format_number(options, 0, length=17)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['122'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['123'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['124'], length=17, decimal_places=2, signed=True, in_currency=True)
# Next casillas
for casilla in (62, 63, 74, 75):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, 0, length=17) # Normally casilla 76 (Regularization of quotas art. 80.Cinco.5ª LIVA)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['46'], length=17, decimal_places=2, signed=True, in_currency=True) # Should normally be casilla 64 (= sum of casillas 46, 58 and 76), but only casilla 46 is in our version of the report
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['65'], length=5, decimal_places=2)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['66'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['77'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('110', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('78', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('87', 0), length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['68'], length=17, decimal_places=2, signed=True, in_currency=True)
if int(year) >= 2026:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('108', 0), length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['69'], length=17, decimal_places=2, signed=True, in_currency=True)
if options['date']['date_from'] >= '2023-01-01':
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['70'], length=17, decimal_places=2, in_currency=True) # Unsigned from 2023 on
else:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['70'], length=17, decimal_places=2, signed=True, in_currency=True)
if options['date']['date_from'] >= '2023-01-01':
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('109', 0.0), length=17, decimal_places=2, in_currency=True)
if int(year) >= 2026:
rslt += self._l10n_es_boe_format_number(options, 0, length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['71'], length=17, decimal_places=2, signed=True, in_currency=True)
# Information about declaration
if options['date']['date_from'] >= '2023-01-01':
rslt += self._l10n_es_boe_format_string(casilla_lines_map['71'] == 0 and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and boe_wizard.previous_report_number or '', length=13)
else:
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and boe_wizard.previous_report_number or '', length=13)
rslt += self._l10n_es_boe_format_string(casilla_lines_map['71'] == 0 and 'X' or ' ')
if options['date']['date_from'] < '2023-01-01':
gov_giving_back = current_company.currency_id.compare_amounts(casilla_lines_map['71'], 0) == -1
partner_bank = boe_wizard.partner_bank_id
bic, iban = self._get_bic_and_iban(partner_bank)
rslt += self._l10n_es_boe_format_string(bic if gov_giving_back and iban and iban[:2] != 'ES' else '', length=11)
rslt += self._l10n_es_boe_format_string(iban, length=34)
# Reserved by AEAT
rslt += self._l10n_es_boe_format_string(' ' * 17)
# Devolución
if gov_giving_back:
bank = partner_bank.bank_id
rslt += self._l10n_es_boe_format_string(bank.name or '', length=70)
rslt += self._l10n_es_boe_format_string(' '.join([bank.street or '', bank.street2 or '']), length=35)
rslt += self._l10n_es_boe_format_string(bank.city or '', length=30)
rslt += self._l10n_es_boe_format_string(bank.country.code or '', length=2)
# Marca SEPA
if iban and boe_wizard.declaration_type != 'N':
iban_country_code = iban[:2]
if iban_country_code == 'ES':
marca = '1'
elif iban_country_code in self.env.ref('base.sepa_zone').mapped('country_ids.code'):
marca = '2'
else:
marca = '3'
else:
marca = '0'
rslt += self._l10n_es_boe_format_string(marca, length=1)
else:
# All those fields must be empty if the report for the current period isn't a return (Devolución),
# the file is rejected if they are not.
rslt += self._l10n_es_boe_format_string('', length=138)
# Reserved by AEAT
reserved_empty_chars = 600
if options['date']['date_to'] >= '2024-09-30':
rslt += self._l10n_es_boe_format_string('rectification_direct_debit' in boe_wizard_fields and boe_wizard.rectification_direct_debit and 'X' or ' ')
if int(year) < 2026:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('108', 0), length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map.get('111', 0), length=17, decimal_places=2, in_currency=True)
if int(year) < 2026:
rslt += self._l10n_es_boe_format_string(' ' * 120)
rslt += self._l10n_es_boe_format_string('rectification_motive_rectifications' in boe_wizard_fields and boe_wizard.rectification_motive_rectifications and 'X' or ' ')
rslt += self._l10n_es_boe_format_string('rectification_motive_discrepancy_adm_crit' in boe_wizard_fields and boe_wizard.rectification_motive_discrepancy_adm_crit and 'X' or ' ')
reserved_empty_chars = 443
elif options['date']['date_from'] < '2022-01-01':
reserved_empty_chars = 445
if int(year) >= 2026:
reserved_empty_chars = 546
rslt += self._l10n_es_boe_format_string(' ' * reserved_empty_chars)
# Footer of page 3
rslt += self._l10n_es_boe_format_string('</T30303000>')
return rslt
def _generate_page_did(self, report, options, current_company, period, year, casilla_lines_map):
rslt = self._l10n_es_boe_format_string('<T303DID00>')
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 303)
partner_bank = boe_wizard.partner_bank_id
bic, iban = self._get_bic_and_iban(partner_bank)
rslt += self._l10n_es_boe_format_string(bic if boe_wizard.declaration_type == 'X' else '', length=11)
rslt += self._l10n_es_boe_format_string(iban, length=34)
# Return in foreign bank account
if boe_wizard.declaration_type == 'X':
bank = partner_bank.bank_id
rslt += self._l10n_es_boe_format_string(bank.name or '', length=70)
rslt += self._l10n_es_boe_format_string(' '.join([bank.street or '', bank.street2 or '']), length=35)
rslt += self._l10n_es_boe_format_string(bank.city or '', length=30)
rslt += self._l10n_es_boe_format_string(bank.country.code or '', length=2)
else:
rslt += self._l10n_es_boe_format_string('', length=137)
# Marca SEPA
if iban and (boe_wizard.declaration_type in ('D', 'X') or casilla_lines_map.get('111')):
iban_country_code = iban[:2]
if iban_country_code == 'ES':
marca = '1'
elif iban_country_code in self.env.ref('base.sepa_zone').mapped('country_ids.code'):
marca = '2'
else:
marca = '3'
else:
marca = '0'
rslt += self._l10n_es_boe_format_string(marca, length=1)
# Reserved by AEAT
rslt += self._l10n_es_boe_format_string(' ' * 617)
# Footer of page 3
rslt += self._l10n_es_boe_format_string('</T303DID00>')
return rslt
class L10n_EsMod347TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod347.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod347)'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 347)
def _report_custom_engine_threshold_insurance_sold(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
domain = MOD_347_CUSTOM_ENGINES_DOMAINS['_report_custom_engine_threshold_insurance_sold']
return self._custom_threshold_common(domain, expressions, options, date_scope, current_groupby, next_groupby, offset=offset, limit=limit)
def _report_custom_engine_threshold_insurance_bought(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
domain = MOD_347_CUSTOM_ENGINES_DOMAINS['_report_custom_engine_threshold_insurance_bought']
return self._custom_threshold_common(domain, expressions, options, date_scope, current_groupby, next_groupby, offset=offset, limit=limit)
def _report_custom_engine_threshold_regular_bought(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
domain = MOD_347_CUSTOM_ENGINES_DOMAINS['_report_custom_engine_threshold_regular_bought']
return self._custom_threshold_common(domain, expressions, options, date_scope, current_groupby, next_groupby, offset=offset, limit=limit)
def _report_custom_engine_threshold_regular_sold(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
domain = MOD_347_CUSTOM_ENGINES_DOMAINS['_report_custom_engine_threshold_regular_sold']
return self._custom_threshold_common(domain, expressions, options, date_scope, current_groupby, next_groupby, offset=offset, limit=limit)
def _report_custom_engine_threshold_all_operations(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
domain = MOD_347_CUSTOM_ENGINES_DOMAINS['_report_custom_engine_threshold_all_operations']
next_groupby = 'move_id' if current_groupby == 'partner_id' else next_groupby
return self._custom_threshold_common(domain, expressions, options, date_scope, current_groupby, next_groupby, offset=offset, limit=limit)
def _custom_threshold_common(self, domain, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None):
""" Some lines of mod 347 report need to be grouped by partner, only keeping the partners whose balance for the line is above 3005.06€.
This function serves as a common helper to the custom engines handling these lines.
"""
report = self.env['account.report'].browse(options['report_id'])
report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else []))
from_fy_dates = self.env.company.compute_fiscalyear_dates(fields.Date.from_string(options['date']['date_from']))
to_fy_dates = self.env.company.compute_fiscalyear_dates(fields.Date.from_string(options['date']['date_to']))
fy_options = {**options, 'date': options['date'].copy()}
# Only adapt dates for the threshold if from and to dates belong to the same fiscal year.
if from_fy_dates == to_fy_dates:
fy_options['date'].update({
'date_from': fields.Date.to_string(from_fy_dates['date_from']),
'date_to': fields.Date.to_string(from_fy_dates['date_to']),
'mode': 'range',
})
# First get all the partners that match the domain but don't reach the threshold. We'll have to exclude them
query = report._get_report_query(fy_options, date_scope, domain=domain + options.get('forced_domain', []))
threshold_value = self._convert_threshold_to_company_currency(3005.06, options)
partners_to_exclude_query = """
SELECT account_move_line.partner_id
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
AND account_move_line.partner_id IS NOT NULL
GROUP BY account_move_line.partner_id
HAVING
ABS(COALESCE(SUM(%(balance_select)s)
FILTER (WHERE account_move_line__move_id.move_type IN ('out_invoice', 'out_refund', 'out_receipt')),
0)) <= %(threshold_value)s
AND
ABS(COALESCE(SUM(%(balance_select)s)
FILTER (WHERE account_move_line__move_id.move_type IN ('in_invoice', 'in_refund', 'in_receipt')),
0)) <= %(threshold_value)s
"""
# Then, add a forced domain because it could be too long later when ast.literal_eval will be applied on it
forced_domain = [
*options.get('forced_domain', []),
('partner_id', 'not in', SQL(
f"({partners_to_exclude_query})",
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(options),
balance_select=report._currency_table_apply_rate(SQL('account_move_line.balance')),
search_condition=query.where_clause,
threshold_value=threshold_value,
))
]
domain_options = {**options, 'forced_domain': forced_domain}
domain_formulas_dict = {str(domain): expressions}
domain_result = report._compute_formula_batch_with_engine_domain(domain_options, date_scope, domain_formulas_dict, current_groupby, next_groupby,
offset=0, limit=None)
return next(result for result in domain_result.values())
def _convert_threshold_to_company_currency(self, threshold, options):
""" Returns a EUR threshold to company currency, using the options' date_to for conversion
"""
threshold_currency = self.env.ref('base.EUR')
if not threshold_currency.active:
raise UserError(_("Currency %s, used for a threshold in this report, is either nonexistent or inactive. Please create or activate it.", threshold_currency.name))
company_currency = self.env.company.currency_id
return threshold_currency._convert(threshold, company_currency, self.env.company, options['date']['date_to'])
def _build_boe_report_options(self, options, year):
return self.env['account.report'].browse(options['report_id']).get_options(
previous_options={
**options,
'date': {'filter': 'custom', 'string': 'Q4 ' + year, 'date_from': year + '-10-01', 'date_to': year + '-12-31', 'mode': 'range'},
'comparison': {
'date_to': year + '-09-30',
'periods': [
{'date_to': year + '-09-30', 'date_from': year + '-07-01', 'string': 'Q3 ' + year, 'mode': 'range'},
{'date_to': year + '-06-30', 'date_from': year + '-04-01', 'string': 'Q2 ' + year, 'mode': 'range'},
{'date_to': year + '-03-31', 'date_from': year + '-01-01', 'string': 'Q1 ' + year, 'mode': 'range'}
],
'number_period': 3,
'string': f'Q3 {year}',
'filter': 'previous_period',
'date_from': f'{year}-07-01',
},
}
)
def _get_required_partner_ids_for_boe(self, mod_invoice_type, date_from, date_to, boe_wizard, operation_key, operation_class):
cash_basis_manual_data = boe_wizard.cash_basis_mod347_data.filtered(lambda x: x.operation_key == operation_key and x.operation_class == operation_class)
all_partners = cash_basis_manual_data.mapped('partner_id')
return set(all_partners.ids)
def _write_type2_header_record(self, current_company, boe_wizard, boe_report_options, year=None):
if not year:
year = str(fields.Date.today().year)
# The header is there once for the whole year. It should use the year as date range and not quarterly. No comparison.
yearly_options = boe_report_options.copy()
del yearly_options['comparison']
yearly_options = self.env['account.report'].browse(boe_report_options['report_id']).get_options(
previous_options={
**yearly_options,
'date': {'date_from': '%s-01-01' % year, 'date_to': '%s-12-31' % year, 'filter': 'custom', 'mode': 'range'},
}
)
rslt = self._l10n_es_boe_format_number(yearly_options, 1)
rslt += self._l10n_es_boe_format_number(yearly_options, 347)
rslt += self._l10n_es_boe_format_string(year, length=4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(current_company.name, length=40)
rslt += self._l10n_es_boe_format_string('T')
rslt += self._l10n_es_boe_format_string(boe_wizard.get_formatted_contact_phone(), length=9)
rslt += self._l10n_es_boe_format_string(boe_wizard.contact_person_name, length=40)
mod_347_boe_sequence = current_company.sudo()._get_mod_boe_sequence("347")
rslt += self._l10n_es_boe_format_number(yearly_options, 347) + self._l10n_es_boe_format_string(mod_347_boe_sequence.next_by_id(), length=10)
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'C' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.substitutive_declaration and 'S' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.previous_report_number or '', length=13, fill_char=b'0', align='right')
declarados_count = self._retrieve_report_expression(yearly_options, 'l10n_es_reports.mod_347_statistics_operations_count_balance')
rslt += self._l10n_es_boe_format_number(yearly_options, declarados_count, length=9)
declarados_total = self._retrieve_report_expression(yearly_options, 'l10n_es_reports.mod_347_operations_title_balance')
rslt += self._l10n_es_boe_format_number(yearly_options, declarados_total, length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
real_estates_data = self._get_real_estates_data(yearly_options, current_company.currency_id)
rslt += self._l10n_es_boe_format_number(yearly_options, real_estates_data['count'], length=9)
rslt += self._l10n_es_boe_format_number(yearly_options, real_estates_data['total'], length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
rslt += self._l10n_es_boe_format_string(' ' * 205)
rslt += self._l10n_es_boe_format_string(' ' * 9) # TIN of the legal representant; blank if 14 years or older
rslt += self._l10n_es_boe_format_string(' ' * 88)
rslt += self._l10n_es_boe_format_string(' ' * 13) # "Sello Electronico" => for administration
rslt += b'\r\n'
return rslt
def _get_real_estates_data(self, boe_report_options, currency_id):
""" Real estates are not directly supported by l10n_es_reports, but by the
submodule l10n_es_real_estates. This function is used as a hook, so that we
don't have to access the result of _write_type2_header_record by indexes
in order to write the real estates data at the right place in the BOE
(which is better in case the code of the header function needs to be extended).
"""
return {'count': 0, 'total': 0}
def _get_invoice_types_xmlids(self):
return {
'l10n_es_reports.mod_347_operations_insurance_bought': 'insurance',
'l10n_es_reports.mod_347_operations_insurance_sold': 'insurance',
'l10n_es_reports.mod_347_operations_regular_sold': 'regular',
'l10n_es_reports.mod_347_operations_regular_bought': 'regular',
}
def _write_type2_partner_record(self, options, report_data, year, current_company, operation_key, manual_parameters_map, insurance=False, local_negocio=False):
currency_id = current_company.currency_id
line_partner = self.env['res.partner'].browse(self.env['account.report']._get_model_info_from_id(report_data['line_data']['id'])[1])
rslt = self._l10n_es_boe_format_number(options, 2)
rslt += self._l10n_es_boe_format_number(options, 347)
rslt += self._l10n_es_boe_format_string(year, length=4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(line_partner.country_id.code == 'ES' and self._extract_spanish_tin(line_partner) or '', length=9)
rslt += self._l10n_es_boe_format_string(' ' * 9) # TIN of the legal representant; blank if 14 years or older
rslt += self._l10n_es_boe_format_string(line_partner.display_name, length=40)
rslt += self._l10n_es_boe_format_string('D') # 'Tipo de hoja', constant
province_code = line_partner.state_id and SPANISH_PROVINCES_REPORT_CODES.get(line_partner.state_id.code) or '99'
rslt += self._l10n_es_boe_format_string(province_code, length=2)
# The country code is only mandatory if there is no province code (hence: no head office in Spain)
if province_code == '99':
if not line_partner.country_id or not line_partner.country_id.code:
raise UserError(_("Partner %(name)s (id %(id)d) is not associated to any Spanish province, and should hence have a country code. For this, fill in its 'country' field.", name=line_partner.name, id=line_partner.id))
if line_partner.country_id.code == 'ES':
raise UserError(_("Partner %(name)s (id %(id)d) is located in Spain but does not have any province. Please set one.", name=line_partner.name, id=line_partner.id))
partner_country_code = line_partner.country_id.code
rslt += self._l10n_es_boe_format_string(partner_country_code if partner_country_code and partner_country_code != 'ES' else '', length=2)
rslt += self._l10n_es_boe_format_string(' ') # Constant
rslt += self._l10n_es_boe_format_string(operation_key, length=1)
# Total amount of operations over the year
year_operations_sum = currency_id.round(sum(i['no_format'] for i in report_data['line_data'].get('columns', [])))
rslt += self._l10n_es_boe_format_number(options, year_operations_sum, length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
rslt += self._l10n_es_boe_format_string(insurance and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(local_negocio and 'X' or ' ')
# En metálico
invoice_types_by_xmlid = self._get_invoice_types_xmlids()
current_invoice_type = invoice_types_by_xmlid[report_data['line_xml_id']]
account_type = operation_key == 'B' and 'asset_receivable' or 'liability_payable'
matching_field = operation_key == 'B' and 'debit' or 'credit'
cash_payments_lines_in_period = self.env['account.move.line'].search([('date', '<=', year + '-12-31'), ('date', '>=', year + '-01-01'), ('journal_id.type', '=', 'cash'), ('payment_id', '!=', False), ('partner_id', '=', line_partner.id), ('account_type', '=', account_type), ('company_id', '=', current_company.id)])
metalico_amount = 0
for cash_payment_aml in cash_payments_lines_in_period:
partial_reconcile_ids = cash_payment_aml['matched_' + matching_field + '_ids']
partial_rec_on_inv_type = partial_reconcile_ids.filtered(lambda x: x[matching_field + '_move_id'].move_id.l10n_es_reports_mod347_invoice_type == current_invoice_type)
for partial_rec in partial_rec_on_inv_type:
metalico_amount += partial_rec.amount
# Context key used for conversion date is set in get_txt.
curr_eur = self.env["res.currency"].search([('name', '=', 'EUR')], limit=1)
threshold = curr_eur._convert(6000, currency_id, current_company, options['date']['date_to'])
if currency_id.compare_amounts(metalico_amount, threshold) == 1: # We only must report this amount if it is above 6000 €
rslt += self._l10n_es_boe_format_number(options, metalico_amount, length=15, decimal_places=2, in_currency=True)
else:
rslt += self._l10n_es_boe_format_number(options, 0, length=15)
# Inmuebles sujetas a la IVA
operation_class = insurance and 'seguros' or local_negocio and 'local_negocio' or 'otras'
real_estates_vat_year_total = 0
real_estates_vat_by_trimester = []
for trimester in range(1, 5):
# This module does not support real estates on its own, but we give the possibility
# to add a real_estates_vat key to the manual parameters map with the needed data,
# through anoter module (l10n_es_real_estates does that)
real_estates_vat_partner_dict = manual_parameters_map.get('real_estates_vat', {}).get(line_partner.id)
real_estates_vat_amount = real_estates_vat_partner_dict and real_estates_vat_partner_dict[str(trimester)][operation_class][operation_key] or 0
real_estates_vat_year_total += real_estates_vat_amount
real_estates_vat_by_trimester.append(real_estates_vat_amount)
real_estates_vat_year_total = currency_id.round(real_estates_vat_year_total)
rslt += self._l10n_es_boe_format_number(options, real_estates_vat_year_total, length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
rslt += self._l10n_es_boe_format_string('0000', length=4) # Ejercicio for metalico operations ; automatic computation not supported
for trimester_index in range(3, -1, -1): # 4th trimester is at position 0 ; 1st at position 3
trimester_total = report_data['line_data'].get('columns', [{} for i in range(0, 4)])[trimester_index].get('no_format', 0)
rslt += self._l10n_es_boe_format_number(options, trimester_total, length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
rslt += self._l10n_es_boe_format_number(options, real_estates_vat_by_trimester[trimester_index], length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
# 'NIF Operador Comunitario'
europe_countries = self.env.ref('base.europe').country_ids - self.env.ref('base.es')
intracom_tin = ''
if line_partner.country_id in europe_countries:
intracom_tin = self._extract_tin(line_partner, error_if_no_tin=False)
rslt += self._l10n_es_boe_format_string(intracom_tin.upper(), length=17)
# Cash Basis (Regimen Especial de Caja)
cash_basis_partner = manual_parameters_map['cash_basis'].get(line_partner.id)
cash_basis_data = cash_basis_partner and cash_basis_partner[operation_class][operation_key] or None
rslt += self._l10n_es_boe_format_string(cash_basis_data is not None and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(line_partner == current_company.partner_id and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(' ') # Not supported by Odoo; according to the partners, too few people need this option
rslt += self._l10n_es_boe_format_number(options, cash_basis_data or 0, length=16, decimal_places=2, signed=True, sign_pos=' ', in_currency=True)
rslt += self._l10n_es_boe_format_string('000000', length=6)
rslt += self._l10n_es_boe_format_string(' ' * 195)
rslt += b'\r\n'
return rslt
def export_boe(self, options):
self.env.flush_all()
dummy, year = self._get_mod_period_and_year(options)
current_company = self.env.company
report = self.env['account.report'].browse(options['report_id'])
# Report options to use to retrieve data for the BOE
boe_report_options = self._build_boe_report_options(options, year)
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 347)
manual_params = boe_wizard.l10n_es_get_partners_manual_parameters_map()
# Header
rslt = self._write_type2_header_record(current_company, boe_wizard, boe_report_options, year=year)
seguros_required_a = self._get_required_partner_ids_for_boe('insurance', year + '-01-01', year + '-12-31', boe_wizard, 'A', 'seguros')
rslt += self._call_on_partner_sublines(
boe_report_options,
'l10n_es_reports.mod_347_operations_insurance_bought',
lambda report_data: self._write_type2_partner_record(boe_report_options, report_data, year, current_company, 'A',
manual_parameters_map=manual_params, insurance=True),
required_ids_set=seguros_required_a
)
# TODO master: remove if. Introduced in a bugfix, so won't necessarily exist.
if self.env.ref('l10n_es_reports.mod_347_operations_insurance_sold', raise_if_not_found=False):
seguros_required_b = self._get_required_partner_ids_for_boe('insurance', year + '-01-01', year + '-12-31', boe_wizard, 'B', 'seguros')
rslt += self._call_on_partner_sublines(
boe_report_options,
'l10n_es_reports.mod_347_operations_insurance_sold',
lambda report_data: self._write_type2_partner_record(boe_report_options, report_data, year, current_company, 'B',
manual_parameters_map=manual_params, insurance=True),
required_ids_set=seguros_required_b
)
otras_required_a = self._get_required_partner_ids_for_boe('regular', year + '-01-01', year + '-12-31', boe_wizard, 'B', 'otras')
rslt += self._call_on_partner_sublines(
boe_report_options,
'l10n_es_reports.mod_347_operations_regular_sold',
lambda report_data: self._write_type2_partner_record(boe_report_options, report_data, year, current_company, 'B',
manual_parameters_map=manual_params),
required_ids_set=otras_required_a
)
otras_required_b = self._get_required_partner_ids_for_boe('regular', year + '-01-01', year + '-12-31', boe_wizard, 'A', 'otras')
rslt += self._call_on_partner_sublines(
boe_report_options,
'l10n_es_reports.mod_347_operations_regular_bought',
lambda report_data: self._write_type2_partner_record(boe_report_options, report_data, year, current_company, 'A',
manual_parameters_map=manual_params),
required_ids_set=otras_required_b
)
return {
'file_name': report.get_default_report_filename(options, 'txt'),
'file_content': rslt,
'file_type': 'txt',
}
def action_audit_cell(self, options, params):
report_line = self.env['account.report.line'].browse(params['report_line_id'])
action = report_line.report_id.action_audit_cell(options, params)
action['context'] = {
**(action.get('context') or {}),
'group_by': ['move_type', 'invoice_date:quarter'],
}
return action
class L10n_EsMod349TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod349.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod349)'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 349)
def _report_custom_engine_modelo349_invoice_e(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='E', move_type='invoice')
def _report_custom_engine_modelo349_invoice_a(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='A', move_type='invoice')
def _report_custom_engine_modelo349_invoice_t(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='T', move_type='invoice')
def _report_custom_engine_modelo349_invoice_s(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='S', move_type='invoice')
def _report_custom_engine_modelo349_invoice_i(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='I', move_type='invoice')
def _report_custom_engine_modelo349_invoice_m(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='M', move_type='invoice')
def _report_custom_engine_modelo349_invoice_h(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='H', move_type='invoice')
def _report_custom_engine_modelo349_invoice_r(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='R', move_type='invoice')
def _report_custom_engine_modelo349_invoice_d(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='D', move_type='invoice')
def _report_custom_engine_modelo349_invoice_c(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='C', move_type='invoice')
def _report_custom_engine_modelo349_refund_e(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='E', move_type='refund')
def _report_custom_engine_modelo349_refund_a(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='A', move_type='refund')
def _report_custom_engine_modelo349_refund_t(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='T', move_type='refund')
def _report_custom_engine_modelo349_refund_s(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='S', move_type='refund')
def _report_custom_engine_modelo349_refund_i(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='I', move_type='refund')
def _report_custom_engine_modelo349_refund_m(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='M', move_type='refund')
def _report_custom_engine_modelo349_refund_h(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='H', move_type='refund')
def _report_custom_engine_modelo349_refund_r(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='R', move_type='refund')
def _report_custom_engine_modelo349_refund_d(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='D', move_type='refund')
def _report_custom_engine_modelo349_refund_c(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, key='C', move_type='refund')
def _report_custom_engine_modelo349_invoice_totals(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, move_type='invoice')
def _report_custom_engine_modelo349_refund_totals(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._custom_modelo349_common(options, current_groupby, move_type='refund')
def _get_modelo349_domain(self, key=False):
domain = [
('account_type', 'in', ('asset_receivable', 'liability_payable')),
('move_id.move_type', 'in', ('in_invoice', 'out_invoice', 'in_refund', 'out_refund')),
('move_id.l10n_es_reports_mod349_available', '=', True),
('tax_line_id', '=', False),
]
if key:
domain += [('move_id.l10n_es_reports_mod349_invoice_type', '=', key)]
else:
domain += [
('move_id.l10n_es_reports_mod349_invoice_type', '!=', False),
'|', ('move_id.reversed_entry_id', '=', False),
'|', ('move_id.reversed_entry_id.amount_residual', '>', 0), ('move_id.amount_residual', '>', 0)
]
return domain
def _get_modelo349_audit_aml_domain(self, options, key, move_type):
res = self._custom_modelo349_common(options, 'move_id', key=key, move_type=move_type)
return [
*self._get_modelo349_domain(key),
('move_id', 'in', tuple(move_id for move_id, _vals in res)),
]
def _custom_modelo349_common(self, options, current_groupby, key=None, move_type=None):
def build_result_dict(query_res_lines, reversed_moves_dict):
result_dict = {'value': 0}
if key:
# Manage all the lines with specific keys, either refunds and invoices
treated_moves = []
for res_line in query_res_lines:
if res_line['move_type'] in ('in_refund', 'out_refund') and res_line['reversed_entry_id']:
# If we find a refund, we need the move which is linked to the refund
matching_move = reversed_moves_dict[res_line['reversed_entry_id']]
if move_type == 'invoice' and options['date']['date_from'] <= matching_move.date.strftime('%Y-%m-%d') <= options['date']['date_to']:
# If the linked move invoice date is in the same period as the report, we just need to subtract the refund value
result_dict['value'] -= res_line['amount_untaxed']
elif move_type == 'refund' and matching_move.date.strftime('%Y-%m-%d') < options['date']['date_from']:
# If the linked move is in a previous period, we need to add to the result the amount_total
# of the linked move minus the sum of it's credit note
# To be sure we don't add a move value twice (in case there is multiple refunds for a same move),
# we add it to a list
# In the case of the file export, we need the amount_total in order to write the rectification lines
if matching_move.id not in treated_moves:
if float_is_zero(matching_move.amount_residual, precision_digits=2) and options.get('export_mode') == 'file':
result_dict['value'] += matching_move.amount_total
result_dict['value'] += matching_move.amount_total - sum(refund.amount_total for refund in matching_move.reversal_move_ids)
treated_moves.append(matching_move.id)
elif ((res_line['move_type'] in ('in_invoice', 'out_invoice') and move_type == 'invoice') or
(res_line['move_type'] in ('in_refund', 'out_refund') and move_type == 'refund')):
# In case the move is matching the section type we always add the value to the result
result_dict['value'] += res_line['amount_untaxed']
elif current_groupby == 'partner_id':
# Compute the sublines for the firsts line (1 & 3)
result_dict['value'] = len([line for line in query_res_lines if (line['move_type'] in ('in_refund', 'out_refund') and move_type == 'refund') or (line['move_type'] in ('in_invoice', 'out_invoice') and move_type == 'invoice')])
else:
# Manage the firsts lines (1 & 3) to display the total number of partners
partner_ids = set()
treated_moves = set()
for res_line in query_res_lines:
if res_line['move_type'] in ('in_invoice', 'out_invoice') and move_type == 'refund':
matching_move = reversed_moves_dict.get(res_line['move_id'])
if matching_move and options['date']['date_from'] <= matching_move.date.strftime('%Y-%m-%d') <= options['date']['date_to']:
# If the linked move invoice date is in the same period as the report, we won't count it in refund operators
treated_moves.add(res_line['move_id'])
elif ((res_line['move_type'] in ('in_invoice', 'out_invoice') and move_type == 'invoice') or
(res_line['move_type'] in ('in_refund', 'out_refund') and move_type == 'refund')):
if res_line['partner_id'] in partner_ids or res_line['reversed_entry_id'] in treated_moves:
continue
partner_ids.add(res_line['partner_id'])
if move_type == 'refund':
reversed_move = reversed_moves_dict.get(res_line['reversed_entry_id'])
if reversed_move and reversed_move.date.strftime('%Y-%m-%d') <= options['date']['date_from']:
result_dict['value'] += 1
else:
result_dict['value'] += 1
result_dict['has_sublines'] = float_compare(result_dict['value'], 0, precision_digits=2)
return result_dict
report = self.env['account.report'].browse(options['report_id'])
report._check_groupby_fields([current_groupby] if current_groupby else [])
domain = self._get_modelo349_domain(key)
# Build query
query = report._get_report_query(options, 'strict_range', domain=domain)
groupby_field_sql = self.env['account.move.line']._field_to_sql('account_move_line', current_groupby, query) if current_groupby else SQL()
self.env.flush_all()
query = SQL("""
SELECT DISTINCT account_move.id AS move_id,
%(select_from_groupby)s
account_move.l10n_es_reports_mod349_invoice_type AS invoice_type,
account_move.move_type AS move_type,
account_move.reversed_entry_id AS reversed_entry_id,
account_move.partner_id AS partner_id,
account_move.amount_untaxed_signed * (CASE WHEN account_move.move_type IN ('in_invoice', 'out_refund') THEN -1 ELSE 1 END) AS amount_untaxed
FROM %(tables)s
JOIN account_move on account_move.id = account_move_line.move_id
WHERE %(where_clause)s
GROUP BY %(group_by)s
account_move.id
""",
select_from_groupby=SQL("%s AS grouping_key,", groupby_field_sql) if groupby_field_sql else SQL(),
tables=query.from_clause,
where_clause=query.where_clause,
group_by=SQL("%s,", groupby_field_sql) if groupby_field_sql else SQL(),
)
self.env.cr.execute(query)
query_res_lines = self.env.cr.dictfetchall()
reversed_entry_ids = {res_line['reversed_entry_id'] for res_line in query_res_lines if res_line['reversed_entry_id']}
reversed_moves_dict = self.env['account.move'].browse(reversed_entry_ids).grouped('id')
if not current_groupby:
return build_result_dict(query_res_lines, reversed_moves_dict)
rslt = []
all_res_per_grouping_key = {}
for query_res in query_res_lines:
grouping_key = query_res['grouping_key']
all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res)
for grouping_key, query_res_lines in all_res_per_grouping_key.items():
result_dict = build_result_dict(query_res_lines, reversed_moves_dict)
if float_compare(result_dict['value'], 0, precision_digits=2):
rslt.append((grouping_key, result_dict))
return rslt
def _write_type1_header_record(self, options, period, year, current_company, boe_wizard):
rslt = self._l10n_es_boe_format_string('1349')
rslt += self._l10n_es_boe_format_string(year, length=4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(current_company.name, length=40)
rslt += self._l10n_es_boe_format_string('T')
rslt += self._l10n_es_boe_format_string(boe_wizard.get_formatted_contact_phone(), length=9)
rslt += self._l10n_es_boe_format_string(boe_wizard.contact_person_name, length=40)
mod_349_boe_sequence = current_company.sudo()._get_mod_boe_sequence("349")
rslt += self._l10n_es_boe_format_number(options, 349) + self._l10n_es_boe_format_string(mod_349_boe_sequence.next_by_id(), length=10)
rslt += self._l10n_es_boe_format_string(boe_wizard.complementary_declaration and 'C' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.substitutive_declaration and 'S' or ' ')
rslt += self._l10n_es_boe_format_string(boe_wizard.previous_report_number or '', length=13, fill_char=b'0', align='right')
rslt += self._l10n_es_boe_format_string(period, length=2)
rslt += self._l10n_es_boe_format_number(options, self._retrieve_report_expression(options, 'l10n_es_reports.mod_349_statistics_invoices_partners_count_balance'), length=9)
rslt += self._l10n_es_boe_format_number(options, self._retrieve_report_expression(options, 'l10n_es_reports.mod_349_statistics_invoices_total_amount_balance'), length=15, in_currency=True, decimal_places=2)
rslt += self._l10n_es_boe_format_number(options, self._retrieve_report_expression(options, 'l10n_es_reports.mod_349_statistics_refunds_partners_count_balance'), length=9)
rslt += self._l10n_es_boe_format_number(options, self._retrieve_report_expression(options, 'l10n_es_reports.mod_349_statistics_refunds_total_amount_balance'), length=15, in_currency=True, decimal_places=2)
rslt += self._l10n_es_boe_format_string(boe_wizard.trimester_2months_report and 'X' or ' ')
rslt += self._l10n_es_boe_format_string(' ' * 204)
rslt += self._l10n_es_boe_format_string(' ' * 9) # TIN of the legal representative, if under 14 years old
rslt += self._l10n_es_boe_format_string(' ' * 101) # Constant
rslt += b'\r\n'
return rslt
def _write_type2_invoice_record(self, options, report_data, year, key, current_company):
rslt = b''
if report_data['line_data']['columns'][0]['no_format'] > 0:
line_partner = self.env['res.partner'].browse(self.env['account.report']._get_model_info_from_id(report_data['line_data']['id'])[1])
rslt += self._l10n_es_boe_format_string('2349')
rslt += self._l10n_es_boe_format_string(year, length=4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(' ' * 58)
rslt += self._l10n_es_boe_format_string(self._extract_tin(line_partner), length=17)
rslt += self._l10n_es_boe_format_string(line_partner.name, length=40)
rslt += self._l10n_es_boe_format_string(key, length=1)
rslt += self._l10n_es_boe_format_number(options, report_data['line_data']['columns'][0]['no_format'], length=13, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_string(' ' * 354)
rslt += b'\r\n'
return rslt
def _write_type2_refund_records(self, options, report_data, current_company, mod_349_type, invoice_report_line_xml_id, report_period, report_year):
line_partner = self.env['res.partner'].browse(self.env['account.report']._get_model_info_from_id(report_data['line_data']['id'])[1])
report_date_from = options['date']['date_from']
report_date_to = options['date']['date_to']
matched_moves = []
period_dict = {}
for refund_invoice in self.env['account.move'].search([
('date', '<=', report_date_to),
('date', '>=', report_date_from),
('move_type', 'in', ['in_refund', 'out_refund']),
('l10n_es_reports_mod349_invoice_type', '=', mod_349_type),
('partner_id', '=', line_partner.id),
('state', '=', 'posted'),
]):
original_invoice = refund_invoice.reversed_entry_id
if not original_invoice:
raise UserError(_('Refund Invoice %s was created without a link to the original invoice that was credited, '
'while we need that information for this report. ', refund_invoice.display_name))
invoice_period, invoice_year = self._retrieve_period_and_year(original_invoice.date, trimester=report_period[-1] == 'T')
if original_invoice.date <= datetime.strptime(report_date_to, '%Y-%m-%d').date() and original_invoice.date >= datetime.strptime(report_date_from, '%Y-%m-%d').date():
continue
if f"{invoice_period}{invoice_year}" not in period_dict:
period_dict[f"{invoice_period}{invoice_year}"] = {
'old_balance': 0,
'new_balance': 0,
}
if original_invoice.id not in matched_moves:
period_dict[f"{invoice_period}{invoice_year}"]['old_balance'] += original_invoice.amount_total
period_dict[f"{invoice_period}{invoice_year}"]['new_balance'] += original_invoice.amount_total - sum(refund.amount_total for refund in original_invoice.reversal_move_ids)
matched_moves.append(original_invoice.id)
rslt = self._l10n_es_boe_format_string('')
# Now, we can report the record !
for period, line_vals in period_dict.items():
rslt += self._l10n_es_boe_format_string('2349')
rslt += self._l10n_es_boe_format_string(report_year, length=4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(' ' * 58)
rslt += self._l10n_es_boe_format_string(self._extract_tin(line_partner), length=17)
rslt += self._l10n_es_boe_format_string(line_partner.name, length=40)
rslt += self._l10n_es_boe_format_string(mod_349_type, length=1)
rslt += self._l10n_es_boe_format_string(' ' * 13) # Constant
rslt += self._l10n_es_boe_format_string(period[2:6], length=4)
rslt += self._l10n_es_boe_format_string(period[0:2], length=2)
rslt += self._l10n_es_boe_format_number(options, line_vals['new_balance'], length=13, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, line_vals['old_balance'], length=13, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_string(' ' * 322)
rslt += b'\r\n'
return rslt
def export_boe(self, options):
period, year = self._get_mod_period_and_year(options)
current_company = self.env.company
# Wizard with manually-entered data
boe_wizard = self._retrieve_boe_manual_wizard(options, 349)
options['export_mode'] = 'file'
if boe_wizard.trimester_2months_report:
if period[-1] == 'T':
options = options.copy()
end_date = datetime.strptime(options['date']['date_to'], '%Y-%m-%d')
options['date']['date_to'] = (end_date + relativedelta(day=31, months=-1)).strftime('%Y-%m-%d')
else:
raise UserError(_("You cannot generate a BOE file for the first two months of a trimester if only one month is selected!"))
# Header
rslt = self._write_type1_header_record(options, period, year, current_company, boe_wizard)
# Invoices lines
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_supplies', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'E', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_acquisitions', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'A', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_triangular', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'T', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_services_sold', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'S', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_services_acquired', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'I', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_supplies_without_taxes', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'M', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_supplies_without_taxes_legal_representative', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'H', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_consignment_sales_agreements', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'R', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_returns_goods_consignment', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'D', current_company))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_substitutions', lambda report_data: self._write_type2_invoice_record(options, report_data, year, 'C', current_company))
# Refunds lines
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_supplies_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'E', 'l10n_es_reports.mod_349_supplies', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_acquisitions_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'A', 'l10n_es_reports.mod_349_acquisitions', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_triangular_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'T', 'l10n_es_reports.mod_349_triangular', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_services_sold_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'S', 'l10n_es_reports.mod_349_services_sold', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_services_acquired_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'I', 'l10n_es_reports.mod_349_services_acquired', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_supplies_without_taxes_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'M', 'l10n_es_reports.mod_349_supplies_without_taxes', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_supplies_without_taxes_legal_representative_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'H', 'l10n_es_reports.mod_349_supplies_without_taxes_legal_representative', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_consignment_sales_agreements_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'R', 'l10n_es_reports.mod_349_consignment_sales_agreements', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_returns_goods_consignment_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'D', 'l10n_es_reports.mod_349_returns_goods_consignment', period, year))
rslt += self._call_on_partner_sublines(options, 'l10n_es_reports.mod_349_substitutions_refunds', lambda report_data: self._write_type2_refund_records(options, report_data, current_company, 'C', 'l10n_es_reports.mod_349_substitutions', period, year))
return {
'file_name': self.env['account.report'].browse(options['report_id']).get_default_report_filename(options, 'txt'),
'file_content': rslt,
'file_type': 'txt',
}
class L10n_EsMod390TaxReportHandler(models.AbstractModel):
_name = 'l10n_es.mod390.tax.report.handler'
_inherit = ['l10n_es.tax.report.handler']
_description = 'Spanish Tax Report Custom Handler (Mod390)'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
super()._append_boe_button(options, 390)
def export_boe(self, options):
_period, year = self._get_mod_period_and_year(options)
boe_wizard = self._retrieve_boe_manual_wizard(options, 390)
current_company = self.env.company
casilla_lines_map = {}
for section in options['sections']:
section_report = self.env['account.report'].browse(section['id'])
report_lines = section_report._get_lines({**options, 'report_id': section_report.id})
casilla_lines_map.update(self._retrieve_casilla_lines(report_lines))
# Header
rslt = self._l10n_es_boe_format_string('<T3900' + year + '0A0000>')
rslt += self._l10n_es_boe_format_string('<AUX>')
rslt += self._l10n_es_boe_format_string(' ' * 70)
odoo_version = odoo.release.version.split('.')
rslt += self._l10n_es_boe_format_string(str(odoo_version[0]) + str(odoo_version[1]), length=4)
rslt += self._l10n_es_boe_format_string(' ' * 4)
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(' ' * 213)
rslt += self._l10n_es_boe_format_string('</AUX>')
rslt += self._generate_mod_390_page1(options, current_company, year, boe_wizard)
rslt += self._generate_mod_390_page2(options, casilla_lines_map)
rslt += self._generate_mod_390_page2b(options, casilla_lines_map)
rslt += self._generate_mod_390_page3(options, casilla_lines_map)
rslt += self._generate_mod_390_page4(options, casilla_lines_map)
# We don't handle page 5 for now (Simplified regime operations, including agricultural, livestock and forestry)
rslt += self._generate_mod_390_page6(options, casilla_lines_map)
rslt += self._generate_mod_390_page7(options, casilla_lines_map)
rslt += self._generate_mod_390_page8(options, casilla_lines_map)
rslt += self._l10n_es_boe_format_string('</T3900' + year + '0A0000>')
return {
'file_name': f'Modelo390 - {year}',
'file_content': rslt,
'file_type': 'txt',
}
def _generate_mod_390_page1(self, options, current_company, year, boe_wizard):
# Main info regarding the company, the representant, the dates and the report itself
# Header
rslt = self._l10n_es_boe_format_string('<T39001000> ')
rslt += self._l10n_es_boe_format_string(self._extract_spanish_tin(current_company.partner_id), length=9)
rslt += self._l10n_es_boe_format_string(current_company.name, length=60)
rslt += self._l10n_es_boe_format_string(boe_wizard.physical_person_name, length=20)
rslt += self._l10n_es_boe_format_string(year)
rslt += self._l10n_es_boe_format_string(' ')
rslt += self._l10n_es_boe_format_string('1' if boe_wizard.monthly_return else '0')
tax_unit_option = options.get('tax_unit')
group_of_entities = True if tax_unit_option and tax_unit_option != 'company_only' else False
rslt += self._l10n_es_boe_format_string('1' if group_of_entities else '0') # Part of a group of entities
rslt += self._l10n_es_boe_format_string(boe_wizard.group_number, length=7) if boe_wizard.group_number else self._l10n_es_boe_format_string(' ' * 7)
rslt += self._l10n_es_boe_format_string('1' if group_of_entities else '0') # Dominant --> True if we're in a tax unit
rslt += self._l10n_es_boe_format_string('0') # Dependant --> always False
rslt += self._l10n_es_boe_format_string('1' if boe_wizard.special_regime_applicable_163 else '0')
# "NIF de la entidad dominante" must only be filled if the declarant is not the dominant entity
# see official documentation : https://sede.agenciatributaria.gob.es/static_files/Sede/Procedimiento_ayuda/G412/instr390.pdf
rslt += self._l10n_es_boe_format_string(' ' * 9)
rslt += self._l10n_es_boe_format_string('2') # Bankrupcy
rslt += self._l10n_es_boe_format_string('1' if boe_wizard.special_cash_basis else '2')
rslt += self._l10n_es_boe_format_string('1' if boe_wizard.special_cash_basis_beneficiary else '2')
rslt += self._l10n_es_boe_format_string('1' if boe_wizard.is_substitute_declaration else '0')
rslt += self._l10n_es_boe_format_string('1' if boe_wizard.is_substitute_decl_by_rectif_of_quotas else '0')
rslt += self._l10n_es_boe_format_string(boe_wizard.previous_decl_number, length=13) if boe_wizard.previous_decl_number else self._l10n_es_boe_format_string(' ' * 13)
rslt += self._l10n_es_boe_format_string(boe_wizard.principal_activity, length=40)
rslt += self._l10n_es_boe_format_string(boe_wizard.principal_code_activity, length=3)
rslt += self._l10n_es_boe_format_string(boe_wizard.principal_iae_epigrafe, length=4)
# Other Activities
for _i in range(0, 5):
# Activity name (40), activity code (3) & activity epigrafe (4)
rslt += self._l10n_es_boe_format_string(' ' * (40 + 3 + 4))
# Joint Declaration
rslt += self._l10n_es_boe_format_string('0')
rslt += self._l10n_es_boe_format_string(' ' * (9 + 37))
# Representant
rslt += self._l10n_es_boe_format_string(' ' * (9 + 80 + 2 + 17 + 5 + 2 + 2 + 2 + 9 + 20 + 15 + 5))
# Personas Jurídicas
# Only one persona juridica is mandatory, the others are left blank
rslt += self._l10n_es_boe_format_string(boe_wizard.judicial_person_name, length=80)
rslt += self._l10n_es_boe_format_string(boe_wizard.judicial_person_nif, length=9)
date = boe_wizard.judicial_person_procuration_date
rslt += self._l10n_es_boe_format_string(datetime.strftime(date, "%d%m%Y") if date else '00000000', length=8)
rslt += self._l10n_es_boe_format_string(boe_wizard.judicial_person_notary, length=12)
rslt += self._l10n_es_boe_format_string(((' ' * (80 + 9)) + '00000000' + (' ' * 12)) * 2)
# Footer of page 1
rslt += self._l10n_es_boe_format_string(' ' * (21 + 13 + 20 + 150)) # Reserved for AEAT
rslt += self._l10n_es_boe_format_string('</T39001000>')
return rslt
def _generate_mod_390_page2(self, options, casilla_lines_map):
# Operations carried out under the general regime : accrued VAT
# Header
rslt = self._l10n_es_boe_format_string('<T39002000> ')
casillas = [700, 701, 667, 668, 1, 2, 702, 703, 669, 670, 3, 4, 5, 6, 704, 705, 671, 672,
500, 501, 706, 707, 673, 674, 502, 503, 504, 505, 708, 709, 675, 676, 643, 644, 710, 711,
677, 678, 645, 646, 647, 648, 712, 713, 679, 680, 7, 8, 714, 715, 681, 682, 9, 10, 11, 12,
13, 14, 716, 717, 683, 684, 21, 22, 718, 719, 685, 686, 23, 24, 25, 26, 720, 721, 687, 688,
545, 546, 722, 723, 689, 690, 547, 548, 551, 552, 27, 28, 29, 30, 649, 650, 31, 32, 33, 34]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[f'{casilla:02d}'],
length=17, decimal_places=2, signed=True, in_currency=True)
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39002000>')
return rslt
def _generate_mod_390_page2b(self, options, casilla_lines_map):
# Header
rslt = self._l10n_es_boe_format_string('<T39002B00> ')
rslt += self._l10n_es_boe_format_string('0' * (17 * 4)) # Reserve space for fields 663, 664, 691, and 692 (4 fixed-width values of 17 characters each)
casillas = [35, 36]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)],
length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_string('0' * (17 * 4)) # Reserve space for fields 665, 666, 693, and 694 (4 fixed-width values of 17 characters each)
casillas = [599, 600, 601, 602, 41, 42, 43, 44, 45, 46, 47]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)],
length=17, decimal_places=2, signed=True, in_currency=True)
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39002B00>')
return rslt
def _generate_mod_390_page3(self, options, casilla_lines_map):
# Operations carried out under the general regime : VAT deductible
# Header
rslt = self._l10n_es_boe_format_string('<T39003000> ')
# Casillas
casillas = [695, 696, 190, 191, 724, 725, 697, 698, 603, 604, 605, 606, 48, 49, 745, 746,
506, 507, 726, 727, 747, 748, 607, 608, 609, 610, 512, 513, 749, 750, 196, 197, 728, 729,
751, 752, 611, 612, 613, 614, 50, 51, 753, 754, 514, 515, 730, 731, 755, 756, 615, 616, 617,
618, 520, 521, 757, 758, 202, 203, 732, 733, 759, 760, 619, 620, 621, 622, 52, 53, 761, 762,
208, 209, 734, 735, 763, 764, 623, 624, 625, 626, 54, 55, 765, 766, 214, 215, 736, 737, 767,
768, 627, 628, 629, 630, 56, 57]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)],
length=17, decimal_places=2, signed=True, in_currency=True)
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39003000>')
return rslt
def _generate_mod_390_page4(self, options, casilla_lines_map):
#Header
rslt = self._l10n_es_boe_format_string('<T39004000> ')
# Casillas
casillas = [769, 770, 220, 221, 738, 739, 771, 772, 631, 632, 633, 634, 58, 59, 773, 774,
587,588, 740, 741, 775, 776, 635, 636, 637, 638, 597, 598, 60, 61, 660, 661, 639, 62, 651,
652, 63, 522, 64, 65]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)],
length=17, decimal_places=2, signed=True, in_currency=True)
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39004000>')
return rslt
def _generate_mod_390_page6(self, options, casilla_lines_map):
# Header
rslt = self._l10n_es_boe_format_string('<T39006000> ')
# Section 7 : Annual settlement result (Only for taxpayers who are taxed exclusively in common territory)
# Casillas
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['658'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['84'], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['659'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['85'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['86'], length=17, decimal_places=2, signed=True, in_currency=True)
# We don't cover the section 8 of the modelo 390, the casillas are replaced by zeros.
# 87 --> 91: Administraciones : Territorio commùn (5), Álava (5), Guipúzcoa(5), Vizcaya(5), Navarra(5)
rslt += self._l10n_es_boe_format_string('0' * 5 * 5)
# 658 : Administraciones - Regularización cuotas art. 80.Cinco.5ª LIVA (17)
# 84 : Administraciones - Suma de resultados (17)
# 92 : Administraciones - Resultado atribuible a territorio común (17)
# 659 : Administraciones -IVA a la importación liquidado por la Aduana (17)
# 93 : Administraciones - Compens. cuotas ej. anterior atrib. territ. com. (17)
# 94 : Administraciones -Resultado liq. anual atribuible territ. comun (17)
rslt += self._l10n_es_boe_format_string('0' * 17 * 6)
# Section 9 : Result of settlements
# Periods that are not taxed under the Special Regime of the group of entities
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['95'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['96'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['524'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['97'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['98'], length=17, decimal_places=2, in_currency=True)
# Periods that are taxed under the Special Regime of the group of entities
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['662'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['525'], length=17, decimal_places=2, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['526'], length=17, decimal_places=2, in_currency=True)
# Section 10 : Trading volume
casillas = [99, 653, 103, 104, 105, 110, 125, 126, 127, 128, 100, 101, 102, 227, 228, 106, 107, 108]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39006000>')
return rslt
def _generate_mod_390_page7(self, options, casilla_lines_map):
# Header
rslt = self._l10n_es_boe_format_string('<T39007000> ')
# Casillas
# Section 11: Specific operations in the carried out during the year
casillas = [230, 109, 231, 232, 111, 113, 523]
for casilla in casillas:
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
for casilla in range(654, 658):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
# We don't cover the section 12 of the modelo 390, the casillas are replaced by blank spaces
for _i in range(0, 5):
rslt += self._l10n_es_boe_format_string(' ' * 40) # Prorratas - Actividad desarrollada
rslt += self._l10n_es_boe_format_string(' ' * 3) # 12. Prorratas - Código CNAE [114]
rslt += self._l10n_es_boe_format_string('0' * 17) # 12. Prorratas - Importe de operaciones [115]
rslt += self._l10n_es_boe_format_string('0' * 17) # 12. Prorratas - Importe de operaciones con derecho a deducción [116]
rslt += self._l10n_es_boe_format_string(' ') # 12. Prorratas - Tipo de prorrata [117]
rslt += self._l10n_es_boe_format_string('0' * 5) # 12. Prorratas - % de prorrata [118]
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39007000>')
return rslt
def _generate_mod_390_page8(self, options, casilla_lines_map):
# Activities with differentiated deduction regimes
# Header
rslt = self._l10n_es_boe_format_string('<T39008000> ')
# Casillas
for casilla in range(139, 153):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['640'], length=17, decimal_places=2, signed=True, in_currency=True)
for casilla in range(153, 170):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['641'], length=17, decimal_places=2, signed=True, in_currency=True)
for casilla in range(170, 187):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map['642'], length=17, decimal_places=2, signed=True, in_currency=True)
for casilla in range(187, 190):
rslt += self._l10n_es_boe_format_number(options, casilla_lines_map[str(casilla)], length=17, decimal_places=2, signed=True, in_currency=True)
# Blank space for AEAT
rslt += self._l10n_es_boe_format_string(' ' * 150)
# Footer
rslt += self._l10n_es_boe_format_string('</T39008000>')
return rslt