odoo18/addons/l10n_es_edi_verifactu/models/account_move.py

346 lines
17 KiB
Python

from odoo import _, api, fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_es_edi_verifactu_required = fields.Boolean(
string="Veri*Factu Required",
related='company_id.l10n_es_edi_verifactu_required',
)
l10n_es_edi_verifactu_document_ids = fields.One2many(
comodel_name='l10n_es_edi_verifactu.document',
inverse_name='move_id',
string="Veri*Factu Documents",
)
l10n_es_edi_verifactu_state = fields.Selection(
string="Veri*Factu Status",
selection=[
('rejected', "Rejected"),
('registered_with_errors', "Registered with Errors"),
('accepted', "Accepted"),
('cancelled', "Cancelled"),
],
compute='_compute_l10n_es_edi_verifactu_state', store=True,
help="""- Rejected: Successfully sent to the AEAT, but it was rejected during validation
- Registered with Errors: Registered at the AEAT, but the AEAT has some issues with the sent document
- Accepted: Registered by the AEAT without errors
- Cancelled: Registered by the AEAT as cancelled""",
)
l10n_es_edi_verifactu_warning_level = fields.Char(
string="Veri*Factu Warning Level",
compute="_compute_l10n_es_edi_verifactu_warning",
)
l10n_es_edi_verifactu_warning = fields.Html(
string="Veri*Factu Warning",
compute="_compute_l10n_es_edi_verifactu_warning",
)
l10n_es_edi_verifactu_qr_code = fields.Char(
string="Veri*Factu QR Code",
compute='_compute_l10n_es_edi_verifactu_qr_code',
)
l10n_es_edi_verifactu_show_cancel_button = fields.Boolean(
string="Show Veri*Factu Cancel Button",
compute='_compute_l10n_es_edi_verifactu_show_cancel_button',
)
l10n_es_edi_verifactu_available_clave_regimens = fields.Char(
string="Available Veri*Factu Regime Key",
compute='_compute_l10n_es_edi_verifactu_available_clave_regimens',
help="Technical field to enable a dynamic selection of the field \"Veri*Factu Regime Key\"",
)
l10n_es_edi_verifactu_clave_regimen = fields.Selection(
string="Veri*Factu Regime Key",
selection='_l10n_es_edi_verifactu_clave_regimen_selection',
compute='_compute_l10n_es_edi_verifactu_clave_regimen', store=True, readonly=False,
)
l10n_es_edi_verifactu_substituted_entry_id = fields.Many2one(
comodel_name='account.move',
string="Substitution of",
index='btree_not_null',
readonly=True,
copy=False,
check_company=True,
)
l10n_es_edi_verifactu_substitution_move_ids = fields.One2many(
string="Substituted by",
comodel_name='account.move',
inverse_name='l10n_es_edi_verifactu_substituted_entry_id',
)
l10n_es_edi_verifactu_refund_reason = fields.Selection(
selection=[
('R1', "R1: Art 80.1 and 80.2 and error of law"),
('R2', "R2: Art. 80.3"),
('R3', "R3: Art. 80.4"),
('R4', "R4: Rest"),
('R5', "R5: Corrective invoices concerning simplified invoices"),
],
string="Veri*Factu Refund Reason",
copy=False,
)
@api.model
def _l10n_es_edi_verifactu_clave_regimen_selection(self):
return [
# There are different possibilities for the ClaveRegimen field
# depending on the Impuesto field (IVA / IGIC)
# Format: '{clave_regimen}' or '{clave_regimen}_{l10n_es_applicability}'
# - The first format is in case the code and label are the same in both lists
# - The second format is in case the code, label pair is only in one of the lists
# VAT & IGIC
('01', _("General regime operation")),
('02', _("Export")),
('11', _("Leasing of business premises")),
# VAT only
('17_iva', _("Operation under one of the regimes provided for in Chapter XI of Title IX (OSS and IOSS).")),
('18_iva', _("Recargo de equivalencia")),
('19_iva', _("Operations of activities included in the Special Regime for Agriculture, Livestock and Fishing (REAGYP)")),
('20_iva', _("Simplified Regime")),
# IGIC only
('17_igic', _("Special retailer regime")),
]
def _l10n_es_edi_verifactu_get_tax_applicability(self):
"""
Currently we only support a single Veri*Factu Tax Applicability per Veri*Factu document.
In `_check_record_values` of model 'l10n_es_edi_verifactu.document' we check:
There is only a single Veri*Factu Tax Applicability on the whole move.
"""
self.ensure_one()
if not self.l10n_es_edi_verifactu_required:
return False
taxes = self.invoice_line_ids.tax_ids.flatten_taxes_hierarchy()
return taxes._l10n_es_edi_verifactu_get_applicability()
@api.model
def _l10n_es_edi_verifactu_get_available_clave_regimens_map(self):
"""
Return dictionary (Veri*Factu Tax Applicability -> set(operation types))
"""
clave_regimen_selection = self._l10n_es_edi_verifactu_clave_regimen_selection()
return {
'01': {ot for ot, _desc in clave_regimen_selection if len(ot.removesuffix('_iva')) == 2},
'03': {ot for ot, _desc in clave_regimen_selection if len(ot.removesuffix('_igic')) == 2},
}
def _l10n_es_edi_verifactu_get_suggested_clave_regimen(self):
"""
Currently we only support a single Clave Regimen per Veri*Factu document.
"""
self.ensure_one()
tax_applicability = self._l10n_es_edi_verifactu_get_tax_applicability()
if not tax_applicability:
return False
taxes = self.invoice_line_ids.tax_ids.flatten_taxes_hierarchy()
special_regime = self.company_id.l10n_es_edi_verifactu_special_vat_regime
return taxes._l10n_es_edi_verifactu_get_suggested_clave_regimen(
special_regime, forced_tax_applicability=tax_applicability
)
@api.depends('invoice_line_ids.tax_ids')
def _compute_l10n_es_edi_verifactu_available_clave_regimens(self):
available_clave_regimens = {
tax_applicability: ','.join(clave_regimens)
for tax_applicability, clave_regimens in self._l10n_es_edi_verifactu_get_available_clave_regimens_map().items()
}
for move in self:
tax_applicability = move._l10n_es_edi_verifactu_get_tax_applicability()
move.l10n_es_edi_verifactu_available_clave_regimens = available_clave_regimens.get(tax_applicability, False)
@api.depends('invoice_line_ids.tax_ids')
def _compute_l10n_es_edi_verifactu_clave_regimen(self):
# Currently we only support one operation type for the whole invoice.
available_clave_regimens = self._l10n_es_edi_verifactu_get_available_clave_regimens_map()
for move in self:
clave_regimen = move.l10n_es_edi_verifactu_clave_regimen
tax_applicability = move._l10n_es_edi_verifactu_get_tax_applicability()
if clave_regimen not in available_clave_regimens.get(tax_applicability, set()):
clave_regimen = move._l10n_es_edi_verifactu_get_suggested_clave_regimen()
move.l10n_es_edi_verifactu_clave_regimen = clave_regimen
@api.depends('l10n_es_edi_verifactu_document_ids', 'l10n_es_edi_verifactu_document_ids.state')
def _compute_l10n_es_edi_verifactu_state(self):
for move in self:
state = move.l10n_es_edi_verifactu_document_ids._get_state()
move.l10n_es_edi_verifactu_state = state
@api.depends('l10n_es_edi_verifactu_document_ids', 'l10n_es_edi_verifactu_document_ids.json_attachment_id')
def _compute_l10n_es_edi_verifactu_qr_code(self):
for move in self:
last_submission = move.l10n_es_edi_verifactu_document_ids._get_last('submission')
url = last_submission._get_qr_code_img_url() if last_submission else False
move.l10n_es_edi_verifactu_qr_code = url
@api.depends('state', 'l10n_es_edi_verifactu_state', 'l10n_es_edi_verifactu_document_ids',
'l10n_es_edi_verifactu_document_ids.state', 'l10n_es_edi_verifactu_document_ids.errors')
def _compute_l10n_es_edi_verifactu_warning(self):
for move in self:
last_document = move.l10n_es_edi_verifactu_document_ids.sorted()[:1]
warning = False
warning_level = False
if last_document.state == 'registered_with_errors':
warning = last_document.errors
warning_level = 'warning'
elif last_document.errors:
warning = last_document.errors
warning_level = 'danger'
elif move.state == 'draft':
if move.l10n_es_edi_verifactu_state:
warning = _("You are modifying a journal entry for which a Veri*Factu document has been sent to the AEAT already.")
warning_level = 'warning'
elif last_document._filter_waiting():
warning = _("You are modifying a journal entry for which a Veri*Factu document is waiting to be sent.")
warning_level = 'warning'
if last_document._filter_waiting():
warning = _("%(existing_warning)sA Veri*Factu document is waiting to be sent as soon as possible.",
existing_warning=(warning + '\n' if warning else ''))
warning_level = warning_level or 'info'
move.l10n_es_edi_verifactu_warning = warning
move.l10n_es_edi_verifactu_warning_level = warning_level
@api.depends('l10n_es_edi_verifactu_state')
def _compute_l10n_es_edi_verifactu_show_cancel_button(self):
for move in self:
move.l10n_es_edi_verifactu_show_cancel_button = move.l10n_es_edi_verifactu_state in ('registered_with_errors', 'accepted')
@api.depends('l10n_es_edi_verifactu_state', 'l10n_es_edi_verifactu_document_ids',
'l10n_es_edi_verifactu_document_ids.state', 'l10n_es_edi_verifactu_document_ids.json_attachment_id')
def _compute_show_reset_to_draft_button(self):
"""
Disallow resetting to draft in the following cases:
* The move is registered (accepted, regsitered_with_errors, cancelled)
* We are waiting to sent a document to the AEAT
"""
# EXTENDS 'account'
super()._compute_show_reset_to_draft_button()
for move in self:
if (move.l10n_es_edi_verifactu_state in ('registered_with_errors', 'accepted', 'cancelled')
or move.l10n_es_edi_verifactu_document_ids._filter_waiting()):
move.show_reset_to_draft_button = False
@api.model
def _l10n_es_edi_verifactu_action_go_to_journal_entry(self, move):
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'account.move',
'res_id': move.id,
'views': [(self.env.ref('account.view_move_form').id, 'form')],
'context': self._context,
}
def l10n_es_edi_verifactu_button_cancel(self):
self._l10n_es_edi_verifactu_mark_for_next_batch(cancellation=True)
def _l10n_es_edi_verifactu_check(self, cancellation=False):
self.ensure_one()
errors = []
if self.state != 'posted':
errors.append(_("The journal entry has to be posted."))
tax_applicability = self._l10n_es_edi_verifactu_get_tax_applicability()
selected_clave_regimen = self.l10n_es_edi_verifactu_clave_regimen
available_clave_regimens_map = self._l10n_es_edi_verifactu_get_available_clave_regimens_map()
if selected_clave_regimen and selected_clave_regimen not in available_clave_regimens_map.get(tax_applicability, set()):
errors.append(_("The Veri*Factu Regime Key is not compatible with the Veri*Factu Tax Applicability."))
return errors
def _l10n_es_edi_verifactu_get_record_values(self, cancellation=False):
self.ensure_one()
company = self.company_id
document_type = 'cancellation' if cancellation else 'submission'
vals = {
'company': company,
'record': self,
'cancellation': cancellation,
'errors': self._l10n_es_edi_verifactu_check(cancellation=cancellation),
'document_vals': {
'move_id': self.id,
'company_id': company.id,
'document_type': document_type,
},
}
if vals['errors']:
return vals
documents = self.l10n_es_edi_verifactu_document_ids
# Just checking whether the last document was rejected is enough; we do not allow to submit the same record
# again after a cancellation (else we get the error '[3000] Registro de facturación duplicado.').
rejected_before = documents._get_last(document_type).state == 'rejected'
tax_applicability = self._l10n_es_edi_verifactu_get_tax_applicability()
selected_clave_regimen = self.l10n_es_edi_verifactu_clave_regimen
clave_regimen = selected_clave_regimen and selected_clave_regimen.split('_', 1)[0]
substituted_move = self.l10n_es_edi_verifactu_substituted_entry_id
reversed_move = self.reversed_entry_id
move_type = self.move_type
if move_type == 'out_invoice' and substituted_move:
verifactu_move_type = 'correction_substitution'
elif move_type == 'out_invoice':
verifactu_move_type = 'invoice'
elif move_type == 'out_refund' and reversed_move.l10n_es_edi_verifactu_substitution_move_ids:
verifactu_move_type = 'reversal_for_substitution'
else:
# move_type == 'out_refund' and not reversed_move.l10n_es_edi_verifactu_substitution_move_ids
verifactu_move_type = 'correction_incremental'
vals.update({
'rejected_before': rejected_before,
'verifactu_state': self.l10n_es_edi_verifactu_state,
'delivery_date': self.delivery_date,
'description': self.invoice_origin[:500] if self.invoice_origin else None,
'invoice_date': self.invoice_date,
'is_simplified': self.l10n_es_is_simplified,
'move_type': move_type,
'verifactu_move_type': verifactu_move_type,
'sign': -1 if move_type == 'out_refund' else 1,
'name': self.name,
'partner': self.commercial_partner_id,
'refund_reason': self.l10n_es_edi_verifactu_refund_reason,
'refunded_document': reversed_move.l10n_es_edi_verifactu_document_ids._get_last('submission'),
'substituted_document': substituted_move.l10n_es_edi_verifactu_document_ids._get_last('submission'),
'substituted_document_reversal_document': substituted_move.reversal_move_ids.l10n_es_edi_verifactu_document_ids._get_last('submission'),
'documents': documents,
'record_identifier': documents._get_last('submission')._get_record_identifier(),
'l10n_es_applicability': tax_applicability,
'clave_regimen': clave_regimen or None,
})
base_amls = self.line_ids.filtered(lambda x: x.display_type == 'product')
base_lines = [self._prepare_product_base_line_for_taxes_computation(x) for x in base_amls]
epd_amls = self.line_ids.filtered(lambda line: line.display_type == 'epd')
base_lines += [self._prepare_epd_base_line_for_taxes_computation(line) for line in epd_amls]
cash_rounding_amls = self.line_ids \
.filtered(lambda line: line.display_type == 'rounding' and not line.tax_repartition_line_id)
base_lines += [self._prepare_cash_rounding_base_line_for_taxes_computation(line) for line in cash_rounding_amls]
tax_amls = self.line_ids.filtered('tax_repartition_line_id')
tax_lines = [self._prepare_tax_line_for_taxes_computation(x) for x in tax_amls]
vals['tax_details'] = self.env['l10n_es_edi_verifactu.document']._get_tax_details(base_lines, company, tax_lines=tax_lines)
return vals
def _l10n_es_edi_verifactu_create_documents(self, cancellation=False):
record_values_list = [
move._l10n_es_edi_verifactu_get_record_values(cancellation=cancellation)
for move in self
]
return {
record_values['record']: self.env['l10n_es_edi_verifactu.document']._create_for_record(record_values)
for record_values in record_values_list
}
def _l10n_es_edi_verifactu_mark_for_next_batch(self, cancellation=False):
document_map = self._l10n_es_edi_verifactu_create_documents(cancellation=cancellation)
self.env['l10n_es_edi_verifactu.document'].trigger_next_batch()
return document_map