568 lines
30 KiB
Python
568 lines
30 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from odoo import api, models, _
|
|
from odoo.exceptions import UserError, RedirectWarning
|
|
from odoo.tools import SQL
|
|
from odoo.tools.float_utils import float_split_str
|
|
|
|
from collections import defaultdict, OrderedDict
|
|
import re
|
|
import zipfile
|
|
import io
|
|
|
|
|
|
class L10n_ArTaxReportHandler(models.AbstractModel):
|
|
_name = 'l10n_ar.tax.report.handler'
|
|
_inherit = ['account.tax.report.handler']
|
|
_description = 'Argentinian Tax Report Custom Handler'
|
|
|
|
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
|
# dict of the form {move_id: {column_group_key: {expression_label: value}}}
|
|
move_info_dict = {}
|
|
|
|
# dict of the form {column_group_key: total_value}
|
|
total_values_dict = {}
|
|
|
|
# Every key/expression_label that is a number (and should be rendered like one)
|
|
number_keys = ['taxed', 'not_taxed', 'vat_25', 'vat_5', 'vat_10', 'vat_21', 'vat_27', 'vat_per', 'perc_iibb', 'perc_earnings', 'city_tax', 'other_taxes', 'total']
|
|
|
|
# Build full query
|
|
query_list = []
|
|
options_per_col_group = report._split_options_per_column_group(options)
|
|
for column_group_key, column_group_options in options_per_col_group.items():
|
|
query = self._build_query(report, column_group_options, column_group_key)
|
|
query_list.append(SQL("(%s)", query))
|
|
|
|
# Set defaults here since the results of the query for this column_group_key might be empty
|
|
total_values_dict.setdefault(column_group_key, dict.fromkeys(number_keys, 0.0))
|
|
|
|
full_query = SQL(" UNION ALL ").join(query_list)
|
|
self.env.cr.execute(full_query)
|
|
results = self.env.cr.dictfetchall()
|
|
for result in results:
|
|
# Iterate over these results in order to fill the move_info_dict dictionary
|
|
move_id = result['id']
|
|
column_group_key = result['column_group_key']
|
|
|
|
# Convert date to string to be displayed in the xlsx report
|
|
result['date'] = result['date'].strftime("%Y-%m-%d")
|
|
|
|
# For number rendering, take the opposite for sales taxes
|
|
sign = -1.0 if result['tax_type'] == 'sale' else 1.0
|
|
|
|
current_move_info = move_info_dict.setdefault(move_id, {})
|
|
|
|
current_move_info['line_name'] = result['move_name']
|
|
current_move_info[column_group_key] = result
|
|
|
|
# Apply sign and add values to totals
|
|
totals = total_values_dict[column_group_key]
|
|
for key in number_keys:
|
|
result[key] = sign * result[key]
|
|
totals[key] += result[key]
|
|
|
|
lines = []
|
|
for move_id, move_info in move_info_dict.items():
|
|
# 1 line for each move_id
|
|
line = self._create_report_line(report, options, move_info, move_id, number_keys)
|
|
lines.append((0, line))
|
|
|
|
# WHen not printing, avoid displaying too many lines (would crash the browser), by using the load_more_limit.
|
|
if not options['export_mode'] and len(lines) > (report.load_more_limit or 0):
|
|
if warnings is not None:
|
|
warnings['l10n_ar_reports.skipped_lines_warning'] = {}
|
|
lines_to_hide = lines[report.load_more_limit:]
|
|
lines = lines[:report.load_more_limit]
|
|
col_indices_to_sum = [
|
|
i
|
|
for i, col_data in enumerate(options['columns'])
|
|
if col_data['expression_label'] in {'taxed', 'not_taxed', 'vat_25', 'vat_5', 'vat_10', 'vat_21', 'vat_27', 'vat_per', 'other_taxes', 'total'}
|
|
]
|
|
|
|
column_sums = defaultdict(float)
|
|
for _line_sequence, line in lines_to_hide:
|
|
for col_index in col_indices_to_sum:
|
|
column_sums[col_index] += line['columns'][col_index]['no_format']
|
|
|
|
lines.append((0, {
|
|
'id': report._get_generic_line_id(None, None, markup='placeholder'),
|
|
'name': _("+%s non-previewed lines", len(lines_to_hide)),
|
|
'columns': [
|
|
{
|
|
'class': 'number',
|
|
'no_format': column_sums[col_index],
|
|
'name': report.format_value(options_per_col_group[col['column_group_key']], column_sums[col_index], figure_type='monetary'),
|
|
}
|
|
if col_index in column_sums
|
|
else {}
|
|
for col_index, col in enumerate(options['columns'])
|
|
],
|
|
'level': 2,
|
|
}))
|
|
|
|
# Single total line if only one type of journal is selected
|
|
if len(self._vat_book_get_selected_tax_types(options)) < 2:
|
|
lines.append((0, self._create_report_total_line(report, options, total_values_dict)))
|
|
|
|
return lines
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
|
|
|
# Add export button
|
|
zip_export_button = {
|
|
'name': _('VAT Book (ZIP)'),
|
|
'sequence': 30,
|
|
'action': 'export_file',
|
|
'action_param': 'vat_book_export_files_to_zip',
|
|
'file_export_type': _('ZIP'),
|
|
'branch_allowed': True,
|
|
}
|
|
|
|
options['buttons'].append(zip_export_button)
|
|
options['ar_vat_book_tax_types_available'] = previous_options.get('ar_vat_book_tax_types_available') or {
|
|
'sale': {'name': _('Sales'), 'selected': True},
|
|
'purchase': {'name': _('Purchases'), 'selected': True},
|
|
}
|
|
if options.get('_running_export_test'):
|
|
# Exporting the file is not allowed for 'purchase'. When executing the export tests, we hence always select 'sales', to avoid raising.
|
|
options['ar_vat_book_tax_types_available']['purchase']['selected'] = False
|
|
|
|
options['forced_domain'] = [
|
|
*options.get('forced_domain', []),
|
|
('l10n_latam_use_documents', '!=', False),
|
|
]
|
|
|
|
tax_types = self._vat_book_get_selected_tax_types(options)
|
|
|
|
# 2 columns are conditional, depending on some taxes being active or inactive
|
|
columns_to_remove = []
|
|
if not self.env['account.tax'].search_count([('type_tax_use', 'in', tax_types), ('tax_group_id.l10n_ar_vat_afip_code', '=', '9')], limit=1):
|
|
columns_to_remove.append('vat_25')
|
|
if not self.env['account.tax'].search_count([('type_tax_use', 'in', tax_types), ('tax_group_id.l10n_ar_vat_afip_code', '=', '8')], limit=1):
|
|
columns_to_remove.append('vat_5')
|
|
|
|
options['columns'] = [col for col in options['columns'] if col['expression_label'] not in columns_to_remove]
|
|
options['custom_display_config'] = {
|
|
'templates': {
|
|
'AccountReportFilters': 'l10n_ar_reports.L10nArTaxReportFiltersCustomizable',
|
|
},
|
|
'components': {
|
|
'AccountReportFilters': 'L10nARTaxReportFilters',
|
|
},
|
|
}
|
|
|
|
####################################################
|
|
# REPORT LINES: CORE
|
|
####################################################
|
|
|
|
def _build_query(self, report, options, column_group_key) -> SQL:
|
|
#pylint: disable=sql-injection
|
|
query = report._get_report_query(options, 'strict_range')
|
|
|
|
tax_types = tuple(self._vat_book_get_selected_tax_types(options))
|
|
|
|
return self.env['account.ar.vat.line']._ar_vat_line_build_query(query.from_clause, query.where_clause, column_group_key, tax_types)
|
|
|
|
def _create_report_line(self, report, options, move_vals, move_id, number_values):
|
|
""" Create a standard (non total) line for the report
|
|
:param options: report options
|
|
:param move_vals: values necessary for the line
|
|
:param move_id: id of the account.move (or account.ar.vat.line)
|
|
:param number_values: list of expression_label that require the 'number' class
|
|
"""
|
|
columns = []
|
|
for column in options['columns']:
|
|
expression_label = column['expression_label']
|
|
value = move_vals.get(column['column_group_key'], {}).get(expression_label)
|
|
|
|
columns.append(report._build_column_dict(value, column, options=options))
|
|
|
|
return {
|
|
'id': report._get_generic_line_id('account.move', move_id),
|
|
'caret_options': 'account.move',
|
|
'name': move_vals['line_name'],
|
|
'columns': columns,
|
|
'level': 2,
|
|
}
|
|
|
|
def _create_report_total_line(self, report, options, total_vals):
|
|
""" Create a total line for the report
|
|
:param options: report options
|
|
:param total_vals: values necessary for the line
|
|
"""
|
|
columns = []
|
|
for column in options['columns']:
|
|
expression_label = column['expression_label']
|
|
value = total_vals.get(column['column_group_key'], {}).get(expression_label)
|
|
|
|
columns.append(report._build_column_dict(value, column, options=options))
|
|
return {
|
|
'id': report._get_generic_line_id(None, None, markup='total'),
|
|
'name': _('Total'),
|
|
'class': 'total',
|
|
'level': 1,
|
|
'columns': columns,
|
|
}
|
|
|
|
####################################################
|
|
# EXPORT/PRINT
|
|
####################################################
|
|
|
|
def vat_book_export_files_to_zip(self, options):
|
|
""" Export method that lets us export the VAT book to a zip archive.
|
|
It contains the files that we upload to ARCA for Purchase VAT Book """
|
|
tax_type = self._vat_book_get_selected_tax_types(options)
|
|
if len(tax_type) > 1:
|
|
raise UserError(_("Only one tax type should be selected."))
|
|
tax_type = tax_type[0]
|
|
|
|
# Build file name
|
|
export_file_name = {'sale': 'Libro_IVA_Ventas', 'purchase': 'Libro_IVA_Compras'}.get(tax_type, 'Libro_IVA')
|
|
export_file_name = f"{export_file_name}_{options['date']['date_to']}"
|
|
|
|
# Build zip content
|
|
txt_types = ['purchases', 'goods_import', 'used_goods'] if tax_type == 'purchase' else ['sale']
|
|
filenames = {
|
|
'purchases': 'Compras',
|
|
'purchases_aliquots': 'IVA_Compras',
|
|
'goods_import': 'Importaciones_de_Bienes',
|
|
'goods_import_aliquots': 'IVA_Importaciones_de_Bienes',
|
|
'used_goods': 'Compras_Bienes_Usados',
|
|
'used_goods_aliquots': 'IVA_Compras_Bienes_Usados',
|
|
'sale': 'Ventas',
|
|
'sale_aliquots': 'IVA_Ventas'
|
|
}
|
|
stream = io.BytesIO()
|
|
with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
|
for txt_type in txt_types:
|
|
options.update({'txt_type': txt_type})
|
|
vouchers_data, aliquots_data = self._vat_book_get_txt_files(options, tax_type)
|
|
if vouchers_data:
|
|
zf.writestr(filenames.get(txt_type) + '.txt', vouchers_data)
|
|
should_write_aliquots = aliquots_data or (not aliquots_data and txt_type == 'purchases')
|
|
if should_write_aliquots:
|
|
zf.writestr(filenames.get(f'{txt_type}_aliquots') + '.txt', aliquots_data)
|
|
file_content = stream.getvalue()
|
|
return {
|
|
'file_name': export_file_name,
|
|
'file_content': file_content,
|
|
'file_type': 'zip',
|
|
}
|
|
|
|
def _vat_book_get_txt_files(self, options, tax_type):
|
|
""" Compute the date to be printed in the txt files"""
|
|
lines = []
|
|
invoices = self._vat_book_get_txt_invoices(options)
|
|
aliquots = self._vat_book_get_REGINFO_CV_ALICUOTAS(options, tax_type, invoices)
|
|
for v in aliquots.values():
|
|
lines += v
|
|
aliquots_data = '\r\n'.join(lines).encode('ISO-8859-1')
|
|
vouchers_data = '\r\n'.join(self._vat_book_get_REGINFO_CV_CBTE(options, aliquots, tax_type, invoices)).encode('ISO-8859-1', 'ignore')
|
|
return vouchers_data, aliquots_data
|
|
|
|
####################################################
|
|
# HELPERS
|
|
####################################################
|
|
|
|
def _vat_book_get_selected_tax_types(self, options):
|
|
# If no particular one is selected, then select them all
|
|
selected_types = [
|
|
selected_type_key
|
|
for selected_type_key, selected_type_value in options['ar_vat_book_tax_types_available'].items()
|
|
if selected_type_value['selected']
|
|
]
|
|
|
|
return selected_types if selected_types else ['sale', 'purchase']
|
|
|
|
@api.model
|
|
def _vat_book_get_lines_domain(self, options):
|
|
company_ids = self.env['account.report'].get_report_company_ids(options)
|
|
selected_journal_types = self._vat_book_get_selected_tax_types(options)
|
|
domain = [('journal_id.type', 'in', selected_journal_types),
|
|
('l10n_latam_use_documents', '=', True), ('company_id', 'in', company_ids)]
|
|
state = options.get('all_entries') and 'all' or 'posted'
|
|
if state and state.lower() != 'all':
|
|
domain += [('state', '=', state)]
|
|
if options.get('date').get('date_to'):
|
|
domain += [('date', '<=', options['date']['date_to'])]
|
|
if options.get('date').get('date_from'):
|
|
domain += [('date', '>=', options['date']['date_from'])]
|
|
return domain
|
|
|
|
@api.model
|
|
def _vat_book_format_amount(self, amount, padding=15, decimals=2):
|
|
""" We need to represent float numbers as integers, with a certain padding and taking into account certain
|
|
decimals to take into account. For example:
|
|
|
|
amount -2.1589 with default padding and decimales
|
|
should return -00000000000215 which is:
|
|
* a integer with padding 15 that includes the sign of the amount if negative
|
|
* the integer part of the amount concatenate with 2 digits of the decimal part of the amount """
|
|
template = "{:0" + str(padding) + "d}"
|
|
(unitary_part, decimal_part) = float_split_str(amount, decimals)
|
|
return template.format(int(unitary_part + decimal_part))
|
|
|
|
@api.model
|
|
def _vat_book_get_partner_document_code_and_number(self, partner):
|
|
""" For a given partner turn the identification coda and identification number in the expected format for the
|
|
txt files """
|
|
# CUIT is mandatory for all except for final consummer
|
|
if partner.l10n_ar_afip_responsibility_type_id.code == '5' or (
|
|
partner.l10n_ar_afip_responsibility_type_id.code == '10' and not partner.commercial_partner_id.is_company):
|
|
doc_code = f"{int(partner.l10n_latam_identification_type_id.l10n_ar_afip_code):0>2d}"
|
|
doc_number = partner.vat or ''
|
|
# we clean the letters that are not supported
|
|
doc_number = re.sub("[^0-9]", "", doc_number)
|
|
elif partner.l10n_ar_afip_responsibility_type_id.code in ('8', '9'):
|
|
commercial_partner = partner.commercial_partner_id
|
|
doc_number = partner.l10n_ar_vat or (commercial_partner.country_id.l10n_ar_legal_entity_vat
|
|
if commercial_partner.is_company else commercial_partner.country_id.l10n_ar_natural_vat)
|
|
doc_code = '80'
|
|
if not commercial_partner.country_id:
|
|
raise RedirectWarning(
|
|
message=_("The partner '%s' does not have a country configured.", commercial_partner.name),
|
|
action={
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'res.partner',
|
|
'views': [(False, 'form')],
|
|
'res_id': commercial_partner.id,
|
|
'name': _('Partner'),
|
|
'view_mode': 'form',
|
|
},
|
|
button_text=_('Edit Partner'),
|
|
)
|
|
if not doc_number:
|
|
raise RedirectWarning(
|
|
message=_(
|
|
"The country '%(country)s' does not have a '%(vat_type)s' configured.",
|
|
country=commercial_partner.country_id.name,
|
|
vat_type=_('Legal Entity VAT') if commercial_partner.is_company else _('Natural Person VAT')
|
|
),
|
|
action={
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'res.country',
|
|
'views': [(False, 'form')],
|
|
'res_id': commercial_partner.country_id.id,
|
|
'name': _('Country'),
|
|
'view_mode': 'form',
|
|
},
|
|
button_text=_('Edit Country'),
|
|
)
|
|
else:
|
|
doc_number = partner.ensure_vat()
|
|
doc_code = '80'
|
|
return doc_code, doc_number.rjust(20, '0')
|
|
|
|
@api.model
|
|
def _vat_book_get_pos_and_invoice_invoice_number(self, invoice):
|
|
res = invoice._l10n_ar_get_document_number_parts(
|
|
invoice.l10n_latam_document_number, invoice.l10n_latam_document_type_id.code)
|
|
return f"{res['invoice_number']:0>20d}", f"{res['point_of_sale']:0>5d}"
|
|
|
|
def _vat_book_get_txt_invoices(self, options):
|
|
state = options.get('all_entries') and 'all' or 'posted'
|
|
if state != 'posted':
|
|
raise UserError(_('Can only generate TXT files using posted entries.'
|
|
' Please remove Include unposted entries filter and try again'))
|
|
|
|
domain = [('l10n_latam_document_type_id.code', '!=', False)] + self._vat_book_get_lines_domain(options)
|
|
txt_type = options.get('txt_type')
|
|
if txt_type == 'purchases':
|
|
domain += [('l10n_latam_document_type_id.code', 'not in', ['66', '30', '32'])]
|
|
elif txt_type == 'goods_import':
|
|
domain += [('l10n_latam_document_type_id.code', '=', '66')]
|
|
elif txt_type == 'used_goods':
|
|
domain += [('l10n_latam_document_type_id.code', 'in', ['30', '32'])]
|
|
return self.env['account.move'].search(domain, order='invoice_date asc, name asc, id asc')
|
|
|
|
def _vat_book_get_tax_row(self, invoice, base, code, tax_amount, options, tax_type):
|
|
inv = invoice
|
|
impo = options.get('txt_type') == 'goods_import'
|
|
|
|
invoice_number, pos_number = self._vat_book_get_pos_and_invoice_invoice_number(inv)
|
|
doc_code, doc_number = self._vat_book_get_partner_document_code_and_number(inv.commercial_partner_id)
|
|
if tax_type == 'sale':
|
|
row = [
|
|
f"{int(inv.l10n_latam_document_type_id.code):0>3d}", # Field 1: Tipo de Comprobante
|
|
pos_number, # Field 2: Punto de Venta
|
|
invoice_number, # Field 3: Número de Comprobante
|
|
self._vat_book_format_amount(base), # Field 4: Importe Neto Gravado
|
|
str(code).rjust(4, '0'), # Field 5: Alícuota de IVA.
|
|
self._vat_book_format_amount(tax_amount), # Field 6: Impuesto Liquidado.
|
|
]
|
|
elif impo:
|
|
row = [
|
|
(inv.l10n_latam_document_number or inv.name or '').rjust(16, '0'), # Field 1: Despacho de importación.
|
|
self._vat_book_format_amount(base), # Field 2: Importe Neto Gravado
|
|
str(code).rjust(4, '0'), # Field 3: Alícuota de IVA
|
|
self._vat_book_format_amount(tax_amount), # Field 4: Impuesto Liquidado.
|
|
]
|
|
else:
|
|
row = [
|
|
f"{int(inv.l10n_latam_document_type_id.code):0>3d}", # Field 1: Tipo de Comprobante
|
|
pos_number, # Field 2: Punto de Venta
|
|
invoice_number, # Field 3: Número de Comprobante
|
|
doc_code, # Field 4: Código de documento del vendedor
|
|
doc_number, # Field 5: Número de identificación del vendedor
|
|
self._vat_book_format_amount(base), # Field 6: Importe Neto Gravado
|
|
str(code).rjust(4, '0'), # Field 7: Alícuota de IVA.
|
|
self._vat_book_format_amount(tax_amount), # Field 8: Impuesto Liquidado.
|
|
]
|
|
return row
|
|
|
|
def _vat_book_get_REGINFO_CV_CBTE(self, options, aliquots, tax_type, invoices):
|
|
res = []
|
|
|
|
for inv in invoices:
|
|
aliquots_count = len(aliquots.get(inv))
|
|
|
|
currency_rate = 1 / inv.invoice_currency_rate
|
|
currency_code = inv.currency_id.l10n_ar_afip_code
|
|
|
|
invoice_number, pos_number = self._vat_book_get_pos_and_invoice_invoice_number(inv)
|
|
doc_code, doc_number = self._vat_book_get_partner_document_code_and_number(inv.partner_id)
|
|
|
|
base_lines, _tax_lines = inv._get_rounded_base_and_tax_lines()
|
|
amounts = inv._l10n_ar_get_amounts(base_lines=base_lines)
|
|
vat_amount = amounts['vat_amount']
|
|
vat_exempt_base_amount = amounts['vat_exempt_base_amount']
|
|
vat_untaxed_base_amount = amounts['vat_untaxed_base_amount']
|
|
other_taxes_amount = amounts['other_taxes_amount']
|
|
vat_perc_amount = amounts['vat_perc_amount']
|
|
iibb_perc_amount = amounts['iibb_perc_amount']
|
|
mun_perc_amount = amounts['mun_perc_amount']
|
|
intern_tax_amount = amounts['intern_tax_amount']
|
|
perc_imp_nacionales_amount = amounts['profits_perc_amount'] + amounts['other_perc_amount']
|
|
if inv.move_type in ('out_refund', 'in_refund') and \
|
|
inv.l10n_latam_document_type_id.code in inv._get_l10n_ar_codes_used_for_inv_and_ref():
|
|
amount_total = -inv.amount_total
|
|
else:
|
|
amount_total = inv.amount_total
|
|
|
|
if vat_exempt_base_amount:
|
|
if inv.partner_id.l10n_ar_afip_responsibility_type_id.code == '10': # free zone operation
|
|
operation_code = 'Z'
|
|
elif inv.l10n_latam_document_type_id.l10n_ar_letter == 'E': # exportation operation
|
|
operation_code = 'X'
|
|
else: # exempt operation
|
|
operation_code = 'E'
|
|
elif inv.l10n_latam_document_type_id.code == '66': # import clearance
|
|
operation_code = 'E'
|
|
elif vat_untaxed_base_amount: # not taxed operation
|
|
operation_code = 'N'
|
|
else:
|
|
operation_code = ' '
|
|
row = [
|
|
inv.invoice_date.strftime('%Y%m%d'), # Field 1: Fecha de comprobante
|
|
f"{int(inv.l10n_latam_document_type_id.code):0>3d}", # Field 2: Tipo de Comprobante.
|
|
pos_number, # Field 3: Punto de Venta
|
|
invoice_number, # Field 4: Número de Comprobante
|
|
# If it is a multiple-sheet receipt, the document number of the first sheet must be reported, taking into account the provisions of article 23, paragraph a), point 6. of General Resolution No. 1,415, the related resolutions that modify and complement this one.
|
|
# In the case of registering grouped by daily totals, the first voucher number of the range to be considered must be entered.
|
|
]
|
|
|
|
if tax_type == 'sale':
|
|
# Field 5: Número de Comprobante Hasta: En el resto de los casos se consignará el dato registrado en el campo 4
|
|
row.append(invoice_number)
|
|
else:
|
|
# Field 5: Despacho de importación
|
|
if inv.l10n_latam_document_type_id.code == '66':
|
|
row.append((inv.l10n_latam_document_number).rjust(16, '0'))
|
|
else:
|
|
row.append(''.rjust(16, ' '))
|
|
row += [
|
|
doc_code, # Field 6: Código de documento del comprador.
|
|
doc_number, # Field 7: Número de Identificación del comprador
|
|
inv.commercial_partner_id.name.ljust(30, ' ')[:30], # Field 8: Apellido y Nombre del comprador.
|
|
self._vat_book_format_amount(amount_total), # Field 9: Importe Total de la Operación.
|
|
self._vat_book_format_amount(vat_untaxed_base_amount), # Field 10: Importe total de conceptos que no integran el precio neto gravado
|
|
]
|
|
|
|
if tax_type == 'sale':
|
|
row += [
|
|
self._vat_book_format_amount(0.0), # Field 11: Percepción a no categorizados
|
|
# the "uncategorized / responsible not registered" figure is not used anymore
|
|
self._vat_book_format_amount(vat_exempt_base_amount), # Field 12: Importe de operaciones exentas
|
|
self._vat_book_format_amount(perc_imp_nacionales_amount + vat_perc_amount), # Field 13: Importe de percepciones o pagos a cuenta de impuestos Nacionales
|
|
]
|
|
else:
|
|
row += [
|
|
self._vat_book_format_amount(vat_exempt_base_amount), # Field 11: Importe de operaciones exentas
|
|
self._vat_book_format_amount(vat_perc_amount), # Field 12: Importe de percepciones o pagos a cuenta del Impuesto al Valor Agregado
|
|
self._vat_book_format_amount(perc_imp_nacionales_amount), # Field 13: Importe de percepciones o pagos a cuenta otros impuestos nacionales
|
|
]
|
|
|
|
row += [
|
|
self._vat_book_format_amount(iibb_perc_amount), # Field 14: Importe de percepciones de ingresos brutos
|
|
self._vat_book_format_amount(mun_perc_amount), # Field 15: Importe de percepciones de impuestos municipales
|
|
self._vat_book_format_amount(intern_tax_amount), # Field 16: Importe de impuestos internos
|
|
str(currency_code), # Field 17: Código de Moneda
|
|
|
|
self._vat_book_format_amount(currency_rate, padding=10, decimals=6), # Field 18: Tipo de Cambio
|
|
# new modality of currency_rate
|
|
|
|
str(aliquots_count), # Field 19: Cantidad de alícuotas de IVA
|
|
operation_code, # Field 20: Código de operación.
|
|
]
|
|
|
|
if tax_type == 'sale':
|
|
document_codes = [
|
|
'16', '19', '20', '21', '22', '23', '24', '27', '28', '29', '33', '34', '35', '37', '38', '43', '44',
|
|
'45', '46', '47', '48', '49', '54', '55', '56', '57', '58', '59', '60', '61', '68', '81', '82', '83',
|
|
'110', '111', '112', '113', '114', '115', '116', '117', '118', '119', '120', '150', '151', '157',
|
|
'158', '159', '160', '161', '162', '163', '164', '165', '166', '167', '168', '169', '170', '171',
|
|
'172', '180', '182', '183', '185', '186', '188', '189', '190', '191',
|
|
'201', '202', '203', '206', '207', '208', '211', '212', '213', '331', '332']
|
|
row += [
|
|
# Field 21: Otros Tributos
|
|
self._vat_book_format_amount(other_taxes_amount),
|
|
|
|
# Field 22: vencimiento comprobante
|
|
# NOTE: it does not appear in instructions but it does in application. for ticket and export invoice is not reported, also for some others but that we do not have implemented
|
|
inv.l10n_latam_document_type_id.code in document_codes and '00000000' or inv.invoice_date_due.strftime('%Y%m%d')
|
|
]
|
|
else:
|
|
row.append(self._vat_book_format_amount(0.0 if inv.company_id.l10n_ar_computable_tax_credit == 'global' else vat_amount)) # Field 21: Crédito Fiscal Computable
|
|
|
|
liquido_type = inv.l10n_latam_document_type_id.code in ['33', '58', '59', '60', '63']
|
|
row += [
|
|
self._vat_book_format_amount(other_taxes_amount), # Field 22: Otros Tributos
|
|
|
|
# NOTE: still not implemented on this three fields for use case with third pary commisioner
|
|
|
|
# Field 23: CUIT Emisor / Corredor
|
|
# It will be reported only if the field 'Tipo de Comprobante' contains '33', '58', '59', '60' or '63'. if there is no intervention of third party in the operation then the informant VAT number will be reported. For the rest of the vouchers it will be completed with zeros
|
|
liquido_type and inv.company_id.partner_id.ensure_vat() or '0' * 11,
|
|
|
|
(liquido_type and inv.company_id.name or '').ljust(30, ' ')[:30], # Field 24: Denominación Emisor / Corredor
|
|
|
|
# Field 25: IVA Comisión
|
|
# If field 23 is different from zero, then we will add the VAT tax base amount of thecommission
|
|
self._vat_book_format_amount(0),
|
|
]
|
|
res.append(''.join(row))
|
|
return res
|
|
|
|
def _vat_book_get_REGINFO_CV_ALICUOTAS(self, options, tax_type, invoices):
|
|
""" We return a dict to calculate the number of aliquots when we make the vouchers """
|
|
res = OrderedDict()
|
|
|
|
# only vat taxes with codes 3, 4, 5, 6, 8, 9. this follows what is mentioned in http://contadoresenred.com/regimen-de-informacion-de-compras-y-ventas-rg-3685-como-cargar-la-informacion/. We start counting codes 1 (not taxed) and 2 (exempt) if there are no aliquots, we add one of this with 0, 0, 0 in details. we also use mapped in case there are duplicate afip codes (eg manual and auto)
|
|
for inv in invoices:
|
|
lines = []
|
|
vat_taxes = inv._get_vat()
|
|
|
|
# typically this is for invoices with zero amount
|
|
if not vat_taxes and any(t.tax_group_id.l10n_ar_vat_afip_code
|
|
and t.tax_group_id.l10n_ar_vat_afip_code != '0'
|
|
for t in inv.invoice_line_ids.mapped('tax_ids')):
|
|
lines.append(''.join(self._vat_book_get_tax_row(inv, 0.0, 3, 0.0, options, tax_type)))
|
|
|
|
# we group by afip_code
|
|
for vat_tax in vat_taxes:
|
|
lines.append(''.join(self._vat_book_get_tax_row(inv, vat_tax['BaseImp'], vat_tax['Id'], vat_tax['Importe'], options, tax_type)))
|
|
|
|
res[inv] = lines
|
|
|
|
return res
|