1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_es_reports/models/libros_export.py
2024-12-10 09:04:09 +07:00

392 lines
20 KiB
Python

import io
import xlsxwriter
from odoo import models, _, api
from odoo.exceptions import UserError
from odoo.tools import get_quarter_number, format_date
from collections import defaultdict
INCOME_FIELDS = (
'year', 'period', 'activity_code', 'activity_type', 'activity_group', 'invoice_type', 'income_concept',
'income_computable', 'date_expedition', 'date_transaction', 'invoice_series', 'invoice_number',
'invoice_final_number', 'partner_nif_type', 'partner_nif_code', 'partner_nif_id',
'partner_name', 'operation_code', 'operation_qualification', 'operation_exempt', 'total_amount',
'base_amount', 'tax_rate', 'taxed_amount', 'surcharge_type', 'surcharge_fee', 'payment_date',
'payment_amount', 'payment_medium', 'payment_medium_id', 'withholding_type', 'withholding_amount',
'billing_agreement', 'property_situation', 'property_reference', 'external_reference'
)
EXPENSE_FIELDS = (
'year', 'period', 'activity_code', 'activity_type', 'activity_group', 'invoice_type', 'expense_concept',
'expense_deductible', 'date_expedition', 'date_transaction', 'expense_series_number', 'expense_final_number',
'date_reception', 'reception_number', 'reception_number_final', 'partner_nif_type', 'partner_nif_code',
'partner_nif_id', 'partner_name', 'operation_code', 'investment_good', 'isp_taxable', 'deductible_later',
'deduction_year', 'deduction_period', 'total_amount', 'base_amount', 'tax_rate', 'taxed_amount', 'tax_deductible',
'surcharge_type', 'surcharge_fee', 'payment_date', 'payment_amount', 'payment_medium', 'payment_medium_id',
'withholding_type', 'withholding_amount', 'billing_agreement', 'property_situation', 'property_reference',
'external_reference'
)
FORMAT_NEEDED_FIELDS = (
'total_amount', 'base_amount', 'tax_rate', 'taxed_amount', 'surcharge_type', 'surcharge_fee',
'income_computable', 'expense_deductible', 'tax_deductible', 'withholding_type', 'withholding_amount'
)
SURCHARGE_TAX_EQUIVALENT = {
5.2: (21,),
1.75: (21,),
1.4: (10,),
0.62: (5,),
0.5: (5, 4),
0: (0,),
}
class GenericTaxReportCustomHandler(models.AbstractModel):
_inherit = 'account.generic.tax.report.handler'
def _custom_options_initializer(self, report, options, previous_options=None):
super()._custom_options_initializer(report, options, previous_options=previous_options)
if self.env.company.account_fiscal_country_id.code == 'ES':
options['buttons'].append({
'name': _('VAT Record Books (XLSX)'),
'sequence': 0,
'action': 'export_file',
'action_param': 'export_libros_de_iva',
'file_export_type': _('XLSX'),
})
def _l10n_es_libros_fill_header(self, sheet_income, sheet_expense):
def fill_header(sheet_val, header_title, subheaders=None):
if not subheaders:
sheet_val['sheet'].merge_range(0, sheet_val['index'], 1, sheet_val['index'], header_title)
sheet_val['index'] += 1
else:
sheet_val['sheet'].merge_range(0, sheet_val['index'], 0, sheet_val['index'] + len(subheaders) - 1, header_title)
for sub_idx, subheader in enumerate(subheaders):
sheet_val['sheet'].write(1, sheet_val['index'] + sub_idx, subheader)
sheet_val['index'] += len(subheaders)
sheet_inc_val = {'sheet': sheet_income, 'index': 0}
sheet_exp_val = {'sheet': sheet_expense, 'index': 0}
for sheet_val in (sheet_inc_val, sheet_exp_val):
fill_header(sheet_val, 'Autoliquidación', ('Ejercicio', 'Periodo'))
fill_header(sheet_val, 'Actividad', ('Código', 'Tipo', 'Grupo o Epígrafe del IAE'))
fill_header(sheet_val, 'Tipo de Factura')
fill_header(sheet_val, 'Concepto de Ingreso' if sheet_val == sheet_inc_val else 'Concepto de Gasto')
fill_header(sheet_val, 'Ingreso Computable' if sheet_val == sheet_inc_val else 'Gasto Deducible')
fill_header(sheet_val, 'Fecha Expedición')
fill_header(sheet_val, 'Fecha Operación')
if sheet_val == sheet_inc_val:
fill_header(sheet_val, 'Identificación de la Factura', ('Serie', 'Número', 'Número-Final'))
fill_header(sheet_val, 'NIF Destinario', ('Tipo', 'Código País', 'Identificación'))
fill_header(sheet_val, 'Nombre Destinario')
else:
fill_header(sheet_val, 'Identificación Factura del Expedidor', ('(Serie-Número)', 'Número-Final'))
fill_header(sheet_val, 'Fecha Recepción')
fill_header(sheet_val, 'Número Recepción')
fill_header(sheet_val, 'Número Recepción Final')
fill_header(sheet_val, 'NIF Expedidor', ('Tipo', 'Código País', 'Identificación'))
fill_header(sheet_val, 'Nombre Expedidor')
fill_header(sheet_val, 'Clave de Operación')
if sheet_val == sheet_inc_val:
fill_header(sheet_val, 'Calificación de la Operación')
fill_header(sheet_val, 'Operación Exenta')
else:
fill_header(sheet_val, 'Bien de Inversión')
fill_header(sheet_val, 'Inversión del Sujeto Pasivo')
fill_header(sheet_val, 'Deducible en Periodo Posterior')
fill_header(sheet_val, 'Periodo Deducción', ('Ejercicio', 'Periodo'))
fill_header(sheet_val, 'Total Factura')
fill_header(sheet_val, 'Base Imponible')
fill_header(sheet_val, 'Tipo de IVA')
if sheet_val == sheet_inc_val:
fill_header(sheet_val, 'Cuota IVA Repercutida')
else:
fill_header(sheet_val, 'Cuota IVA Soportado')
fill_header(sheet_val, 'Cuota Deducible')
fill_header(sheet_val, 'Tipo de Recargo eq.')
fill_header(sheet_val, 'Cuota Recargo eq.')
if sheet_val == sheet_inc_val:
head = 'Cobro (Operación Criterio de Caja de IVA y/o artículo 7.2.1º de Reglamento del IRPF)'
else:
head = 'Pago (Operación Criterio de Caja de IVA y/o artículo 7.2.1º de Reglamento del IRPF)'
fill_header(sheet_val, head, ('Fecha', 'Importe', 'Medio Utilizado', 'Identificación Medio Utilizado'))
fill_header(sheet_val, 'Tipo Retención del IRPF')
fill_header(sheet_val, 'Importe Retenido del IRPF')
fill_header(sheet_val, 'Registro Acuerdo Facturación')
fill_header(sheet_val, 'Inmueble', ('Situación', 'Referencia Catastral'))
fill_header(sheet_val, 'Referencia Externa')
def _l10n_es_libros_get_common_line_vals(self, line, tax):
iae_group = self.env.company.l10n_es_reports_iae_group
partner = line.partner_id
exempt_reason = line.move_id.invoice_line_ids.tax_ids.filtered(lambda t: t.l10n_es_exempt_reason == 'E2')
sign = -1 if line.move_id.is_sale_document(include_receipts=True) else 1
common_line_vals = {
'year': line.date.year,
'period': str(get_quarter_number(line.date)) + 'T',
'activity_code': iae_group[0],
'activity_type': iae_group[1:3],
'activity_group': iae_group[3:],
'invoice_type': {
'out_invoice': 'F2' if line.move_id.l10n_es_is_simplified else 'F1',
'out_receipt': 'F2' if line.move_id.l10n_es_is_simplified else 'F1',
'out_refund': 'R5' if line.move_id.l10n_es_is_simplified else 'R1',
'in_invoice': 'F5' if tax.l10n_es_type == 'dua' else 'F1',
'in_receipt': 'F5' if tax.l10n_es_type == 'dua' else 'F1',
'in_refund': 'R4',
}[line.move_type],
'date_expedition': format_date(self.env, line.date.isoformat(), date_format='MM/dd/yyyy'),
'date_transaction': format_date(self.env, line.invoice_date.isoformat(),
date_format='MM/dd/yyyy') if line.date != line.invoice_date else '',
'partner_name': partner.name,
'operation_code': '02' if exempt_reason else '01',
'total_amount': line.balance * sign,
'base_amount': line.balance * sign,
'tax_rate': 0,
'taxed_amount': 0,
'surcharge_type': 0,
'surcharge_fee': 0,
'withholding_type': 0,
'withholding_amount': 0,
}
if (not partner.country_id or partner.country_id.code == 'ES') and partner.vat:
common_line_vals['partner_nif_id'] = partner.vat[2:] if partner.vat.startswith('ES') else partner.vat
elif partner.vat and partner.country_id in self.env.ref('base.europe').country_ids:
common_line_vals['partner_nif_id'] = partner.vat
common_line_vals['partner_nif_type'] = "02"
elif partner.vat:
common_line_vals['partner_nif_id'] = partner.vat
common_line_vals['partner_nif_type'] = "06"
common_line_vals['partner_nif_code'] = partner.country_id.code
return common_line_vals
def _l10n_es_libros_create_income_line_vals(self, line, tax):
line_vals = {field: '' for field in INCOME_FIELDS}
line_vals.update(self._l10n_es_libros_get_common_line_vals(line, tax))
line_vals.update({
'income_concept': 'I01',
'income_computable': -line.balance,
'invoice_number': line.move_id.name,
'operation_qualification': {
'sujeto': 'S1',
'sujeto_isp': 'S2',
'no_sujeto': 'N1',
'no_sujeto_loc': 'N2',
}.get(tax.l10n_es_type, ''),
'operation_exempt': tax.l10n_es_exempt_reason if tax.l10n_es_type == 'exento' else '',
})
if line_vals['operation_qualification'] == 'S2':
line_vals['tax_rate'] = 0
return line_vals
def _l10n_es_libros_create_expense_line_vals(self, line, tax):
expense_concept = 'G01'
line_vals = {field: '' for field in EXPENSE_FIELDS}
line_vals.update(self._l10n_es_libros_get_common_line_vals(line, tax))
line_vals.update({
'expense_concept': expense_concept,
'expense_deductible': line.balance,
'expense_series_number': line.move_id.name,
'date_reception': format_date(self.env, line.date.isoformat(), date_format='MM/dd/yyyy'),
'investment_good': 'S' if (tax.l10n_es_bien_inversion and
line_vals['operation_code'] != '02') else 'N',
'isp_taxable': 'S' if tax.l10n_es_type == 'sujeto_isp' else 'N',
'tax_deductible': 0,
})
return line_vals
@api.model
def _l10n_es_libros_merge_base_line(self, line_vals, base_line):
is_income = base_line.move_id.is_sale_document(include_receipts=True)
sign = -1 if is_income else 1
new_balance = line_vals['base_amount'] + base_line.balance * sign
line_vals.update({
'total_amount': new_balance,
'base_amount': new_balance,
})
if is_income:
line_vals['income_computable'] = new_balance
else:
line_vals['expense_deductible'] = new_balance
# TODO: remove this method in master
def _merge_tax_line(self, line_vals, tax_line):
return
# TODO: remove this method in master
def _merge_surcharge_line(self, line_vals, surcharge_line):
return
# TODO: remove this method in master
def _merge_tax(self, line_vals, move, tax, tax_amount):
return
@api.model
def _l10n_es_libros_merge_line_tax(self, line_vals, line, tax, tax_amount):
if tax.l10n_es_type == 'recargo':
line_vals.update({
'total_amount': line_vals['total_amount'] + tax_amount,
'surcharge_type': abs(tax.amount),
'surcharge_fee': tax_amount,
})
elif tax.l10n_es_type == 'retencion':
line_vals.update({
'total_amount': line_vals['total_amount'] + tax_amount,
'withholding_type': abs(tax.amount),
'withholding_amount': -tax_amount,
})
elif tax.l10n_es_type == 'ignore':
return
else:
if line_vals.get('operation_qualification') == 'S2':
return
line_vals.update({
'total_amount': line_vals['total_amount'] + tax_amount,
'tax_rate': tax.amount,
'taxed_amount': line_vals['taxed_amount'] + tax_amount,
})
# add amount to tax_deductible only if the line have mod303 in the tax grid (supports pro rata tax type)
if not line.move_id.is_sale_document(include_receipts=True) and any('mod303' in tag for tag in line.tax_tag_ids.mapped('name')):
line_vals['tax_deductible'] += tax_amount
def _l10n_es_libros_format_sheet_line_vals(self, sheet_line_vals):
for move_idx in sheet_line_vals:
for line_vals in sheet_line_vals[move_idx].values():
for field, value in line_vals.items():
if field in FORMAT_NEEDED_FIELDS and value != '':
line_vals[field] = round(value, 2)
def _l10n_es_libros_get_sheet_line_vals(self, lines):
""" Parse the invoice lines to generate each report lines based on the combination
of taxes used on each line.
Then parse the tax lines to populate the fields related to tax amounts of the report lines.
"""
inc_line_vals, exp_line_vals = {}, {}
# The keys of the first dict are account moves (account.move record).
# The keys of the second dict are a tuple of the taxes used on each invoice line: (tax_a, tax_b).
# The keys of the third dict are taxes (account.tax record).
# One entry of the third dict is an accumulated amount of base amounts for a tax, which is used
# to compute the ratio with the base amount of a tax line.
base_amount_by_tax = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
# generate the report lines from the invoice lines
for line in lines.filtered(lambda l: l.tax_ids):
is_income = line.move_id.is_sale_document(include_receipts=True)
sheet_line_vals = inc_line_vals if is_income else exp_line_vals
create_line_vals = self._l10n_es_libros_create_income_line_vals if is_income else self._l10n_es_libros_create_expense_line_vals
move = line.move_id
sheet_line_vals.setdefault(move.id, {})
taxes = line.tax_ids.flatten_taxes_hierarchy()
tax_key = tuple(taxes)
ignore_line = True
regular_tax = False
for tax in taxes:
# a tax of type "recargo" should be associated to a tax having a specific amount
if tax.l10n_es_type == 'recargo':
linked_tax = taxes.filtered(
lambda t: t.l10n_es_type not in ('recargo', 'retencion', 'ignore')
and t.amount in SURCHARGE_TAX_EQUIVALENT[tax.amount]
)
if not linked_tax:
raise UserError(_('Unable to find matching surcharge tax in %s', move.name))
# invoice lines with only taxes of type "ignore" and/or "retencion" should be ignored
elif tax.l10n_es_type not in ('ignore', 'retencion'):
ignore_line = False
if not regular_tax:
regular_tax = tax
# compute the new accumulated base amount for this tax
base_amount_by_tax[move][tax_key][tax] += abs(line.balance)
# if ignore_line is True, then all the taxes are "ignore" and/or "retencion" ones
if ignore_line:
del base_amount_by_tax[move][tax_key]
continue # no report line should be created for such line
# initialize [inc/exp]_line_vals with base balance and first regular tax of invoice lines
if tax_key in sheet_line_vals[move.id]:
self._l10n_es_libros_merge_base_line(sheet_line_vals[move.id][tax_key], line)
else:
sheet_line_vals[move.id][tax_key] = create_line_vals(line, regular_tax)
# loop on each tax line and compute the tax amount for each line based on the ratio
# of the base amount
tax_lines = (line for line in lines if line.tax_line_id)
for line in tax_lines:
is_income = line.move_id.is_sale_document(include_receipts=True)
sheet_line_vals = inc_line_vals if is_income else exp_line_vals
move, tax = line.move_id, line.tax_line_id
sign = -1 if is_income else 1
remaining_tax_base_amount = line.tax_base_amount
remaining_tax_balance = line.balance
for tax_key, data in base_amount_by_tax[move].items():
if tax not in tax_key:
continue
remaining_tax_base_amount -= data[tax]
if remaining_tax_base_amount <= 0:
tax_amount = remaining_tax_balance
remaining_tax_balance = 0
else:
ratio = data[tax] / line.tax_base_amount if line.tax_base_amount else 0
tax_amount = move.company_id.currency_id.round(line.balance * ratio)
remaining_tax_balance -= tax_amount
# update the report line with the tax amount
self._l10n_es_libros_merge_line_tax(sheet_line_vals[move.id][tax_key], line, tax, tax_amount * sign)
self._l10n_es_libros_format_sheet_line_vals(inc_line_vals)
self._l10n_es_libros_format_sheet_line_vals(exp_line_vals)
return inc_line_vals, exp_line_vals
def _l10n_es_libros_fill_content(self, sheet_income, sheet_expense, report, options):
domain = report._get_options_domain(options, 'strict_range') + [('move_type', '!=', 'entry')]
lines = self.env['account.move.line'].search(domain)
inc_line_vals, exp_line_vals = self._l10n_es_libros_get_sheet_line_vals(lines)
sheet_inc_vals = {'sheet': sheet_income, 'line_vals': inc_line_vals, 'row_idx': 2, 'fields': INCOME_FIELDS}
sheet_exp_vals = {'sheet': sheet_expense, 'line_vals': exp_line_vals, 'row_idx': 2, 'fields': EXPENSE_FIELDS}
for sheet_vals in (sheet_inc_vals, sheet_exp_vals):
for move_idx in sheet_vals['line_vals']:
for line_vals in sheet_vals['line_vals'][move_idx].values():
for col_idx, field in enumerate(sheet_vals['fields']):
if field in FORMAT_NEEDED_FIELDS and line_vals[field] and options.get('number_format'):
sheet_vals['sheet'].write(sheet_vals['row_idx'], col_idx, line_vals[field], options.get('number_format'))
else:
sheet_vals['sheet'].write(sheet_vals['row_idx'], col_idx, line_vals[field])
sheet_vals['row_idx'] += 1
def export_libros_de_iva(self, options):
report = self.env['account.report'].browse(options['report_id'])
output = io.BytesIO()
workbook = xlsxwriter.Workbook(output, {'in_memory': True, 'strings_to_formulas': False})
number_format = workbook.add_format({'num_format': '0.00'})
sheet_income = workbook.add_worksheet('EXPEDIDAS_INGRESOS')
sheet_expense = workbook.add_worksheet('RECIBIDAS_GASTOS')
options['number_format'] = number_format
self._l10n_es_libros_fill_header(sheet_income, sheet_expense)
self._l10n_es_libros_fill_content(sheet_income, sheet_expense, report, options)
workbook.close()
output.seek(0)
generated_file = output.read()
output.close()
return {
'file_name': 'libros_registro_de_iva.xlsx',
'file_content': generated_file,
'file_type': 'xlsx',
}
class SpanishLibrosRegistroExportHandler(models.AbstractModel): # TODO: Remove in master
_name = 'l10n_es.libros.registro.export.handler'
_inherit = 'account.generic.tax.report.handler'
_description = 'Spanish Libros Registro de IVA'