Odoo18-Base/addons/l10n_ro_edi/models/account_move.py

263 lines
13 KiB
Python
Raw Permalink Normal View History

2025-01-06 10:57:38 +07:00
import requests
from odoo import models, fields, _, api, modules, tools
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_ro_edi_document_ids = fields.One2many(
comodel_name='l10n_ro_edi.document',
inverse_name='invoice_id',
)
l10n_ro_edi_state = fields.Selection(
selection=[
('invoice_sent', 'Sent'),
('invoice_validated', 'Validated'),
],
string='E-Factura Status',
compute='_compute_l10n_ro_edi_state',
store=True,
help="""- Sent: Successfully sent to the SPV, waiting for validation
- Validated: Sent & validated by the SPV
- Error: Sending error or validation error from the SPV""",
)
l10n_ro_edi_attachment_id = fields.Many2one(comodel_name='ir.attachment')
l10n_ro_edi_index = fields.Char(string='E-Factura Index', readonly=True)
################################################################################
# Compute Methods
################################################################################
@api.depends('l10n_ro_edi_document_ids')
def _compute_l10n_ro_edi_state(self):
self.l10n_ro_edi_state = False
for move in self:
for document in move.l10n_ro_edi_document_ids.sorted():
if document.state in ('invoice_sent', 'invoice_validated'):
move.l10n_ro_edi_state = document.state
break
@api.depends('l10n_ro_edi_state')
def _compute_show_reset_to_draft_button(self):
""" Prevent user to reset move to draft when there's an
active sending document or a successful response has been received """
# EXTENDS 'account'
super()._compute_show_reset_to_draft_button()
for move in self:
if move.l10n_ro_edi_state in ('invoice_sent', 'invoice_validated'):
move.show_reset_to_draft_button = False
################################################################################
# Romanian Document Shorthands & Helpers
################################################################################
def _l10n_ro_edi_create_attachment_values(self, raw, res_model=None, res_id=None):
""" Shorthand for creating the attachment_id values on the invoice's document """
self.ensure_one()
res_model = res_model or self._name
res_id = res_id or self.id
return {
'name': f"ciusro_signature_{self.name.replace('/', '_')}.xml",
'res_model': res_model,
'res_id': res_id,
'raw': raw,
'type': 'binary',
'mimetype': 'application/xml',
}
def _l10n_ro_edi_create_document_invoice_sent(self, values: dict):
""" Shorthand for creating a ``l10n_ro_edi.document`` of state ``invoice_sent``.
:param values: dictionary of {'key_loading': <str>, 'attachment_raw': <bytes>}
:return: ``l10n_ro_edi.document`` object """
self.ensure_one()
document = self.env['l10n_ro_edi.document'].sudo().create({
'invoice_id': self.id,
'state': 'invoice_sent',
'key_loading': values['key_loading'],
})
attachment_values = self._l10n_ro_edi_create_attachment_values(
raw=values['attachment_raw'],
res_model=document._name,
res_id=document.id,
)
document.attachment_id = self.env['ir.attachment'].sudo().create(attachment_values)
return document
def _l10n_ro_edi_create_document_invoice_sending_failed(self, values: dict):
""" Shorthand for creating a ``l10n_ro_edi.document`` of state ``invoice_sending_failed``.
The ``attachment_raw`` and ``key_loading`` dictionary values is optional in case the error is from pre_send.
:param values: dictionary of {
'error': <str>,
'key_loading': <optional str>,
'attachment_raw': <optional str>,
}
:return: ``l10n_ro_edi.document`` object """
self.ensure_one()
document = self.env['l10n_ro_edi.document'].sudo().create({
'invoice_id': self.id,
'state': 'invoice_sending_failed',
'message': _("Error when sending the document to the SPV:\n%s", values['error']),
})
if values.get('key_loading'):
document.key_loading = values['key_loading']
if values.get('attachment_raw'):
attachment_values = self._l10n_ro_edi_create_attachment_values(
raw=values['attachment_raw'],
res_model=document._name,
res_id=document.id,
)
document.attachment_id = self.env['ir.attachment'].sudo().create(attachment_values)
return document
def _l10n_ro_edi_create_document_invoice_validated(self, values: dict):
""" Shorthand for creating a ``l10n_ro_edi.document`` of state `invoice_validated`.
The created attachment are saved on both the document and on the invoice.
:param values: dictionary containing 'key_loading', 'key_signature', 'key_certificate', and 'attachment_raw'
:return: ``l10n_ro_edi.document`` object """
self.ensure_one()
document = self.env['l10n_ro_edi.document'].sudo().create({
'invoice_id': self.id,
'state': 'invoice_validated',
'key_loading': values['key_loading'],
'key_signature': values['key_signature'],
'key_certificate': values['key_certificate'],
})
attachment = self.env['ir.attachment'].sudo().create(self._l10n_ro_edi_create_attachment_values(values['attachment_raw']))
document.attachment_id = self.l10n_ro_edi_attachment_id = attachment
return document
def _l10n_ro_edi_get_attachment_file_name(self):
""" Returns the signature file attachment's name from ``l10n_ro_edi.document``/``invoice_validated`` """
self.ensure_one()
return f"ciusro_{self.name.replace('/', '_')}.xml"
def _l10n_ro_edi_get_failed_documents(self):
""" Shorthand for getting all l10n_ro_edi.document in invoice_sending_failed state """
self.ensure_one()
return self.l10n_ro_edi_document_ids.filtered(lambda d: d.state == 'invoice_sending_failed')
def _l10n_ro_edi_get_sent_and_failed_documents(self):
""" Shorthand for getting all l10n_ro_edi.document in ``invoice_sent`` and ``invoice_sending_failed`` state """
self.ensure_one()
return self.l10n_ro_edi_document_ids.filtered(lambda d: d.state in ('invoice_sent', 'invoice_sending_failed'))
################################################################################
# Send Logics
################################################################################
def _l10n_ro_edi_get_pre_send_errors(self, xml_data='', assert_xml=False):
""" Compute all possible common errors before sending the XML to the SPV """
self.ensure_one()
errors = []
if not self.company_id.l10n_ro_edi_access_token:
errors.append(_('Romanian access token not found. Please generate or fill it in the settings.'))
if not xml_data and assert_xml:
errors.append(_('CIUS-RO XML attachment not found.'))
return errors
def _l10n_ro_edi_send_invoice(self, xml_data):
"""
This method send xml_data to the Romanian SPV using the single invoice's (self) data.
The invoice's company and move_type will be used to calculate the required params in the send request.
The state of the document deletion/creation are as follows:
- Pre-check any errors from the invoice's pre_send check before sending
- if error -> delete all error documents, create a new error document
- else -> continue to the next step
- Send to E-Factura, and based on the result:
- if error -> delete all error documents, create a new error document
- if success -> delete all error & sending documents, create a new sending document
:param xml_data: string of the xml data to be sent
"""
self.ensure_one()
if errors := self._l10n_ro_edi_get_pre_send_errors(xml_data, True):
self._l10n_ro_edi_get_failed_documents().unlink()
self._l10n_ro_edi_create_document_invoice_sending_failed({'error': '\n'.join(errors)})
return
self.env['res.company']._with_locked_records(self)
result = self.env['l10n_ro_edi.document']._request_ciusro_send_invoice(
company=self.company_id,
xml_data=xml_data,
move_type=self.move_type,
)
result['attachment_raw'] = xml_data
if 'error' in result: # result == {'error': <str>, 'attachment_raw': <bytes>}
self._l10n_ro_edi_get_failed_documents().unlink()
self._l10n_ro_edi_create_document_invoice_sending_failed(result)
else: # result == {'key_loading': <str>, 'attachment_raw': <bytes>}; initial sending successful
self._l10n_ro_edi_get_sent_and_failed_documents().unlink()
self._l10n_ro_edi_create_document_invoice_sent(result)
self.l10n_ro_edi_index = result['key_loading']
self.message_post(body=_(
"E-Factura has been sent and is now being validated by the SPV with index key: %s",
result['key_loading'],
))
def _l10n_ro_edi_fetch_invoice_sent_documents(self):
"""
This method loops over all invoice with sending document in `self`. For each of them,
it pre-checks error and make a fetch request for the invoice. Based on the answer, it will then:
- if no answer is received, it will do nothing on the selected invoice
- if error -> delete all errors, create a new error document
- else (receives `key_download`) -> immediately make a download request and process it:
- if error -> delete all sending and error documents, create a new error document
- if success -> delete all sending and error documents, create success document
"""
session = requests.Session()
to_delete_documents = self.env['l10n_ro_edi.document']
invoices_to_fetch = self.filtered(lambda inv: inv.l10n_ro_edi_state == 'invoice_sent')
for invoice in invoices_to_fetch:
if errors := invoice._l10n_ro_edi_get_pre_send_errors():
to_delete_documents |= invoice._l10n_ro_edi_get_failed_documents()
invoice._l10n_ro_edi_create_document_invoice_sending_failed({'error': '\n'.join(errors)})
continue
active_sending_document = invoice.l10n_ro_edi_document_ids.filtered(lambda d: d.state == 'invoice_sent')[0]
previous_raw = active_sending_document.attachment_id.sudo().raw
self.env['res.company']._with_locked_records(invoices_to_fetch)
result = self.env['l10n_ro_edi.document']._request_ciusro_fetch_status(
company=invoice.company_id,
key_loading=invoice.l10n_ro_edi_index,
session=session,
)
if result == {}: # SPV is still processing the XML (no answer yet); do nothing
continue
elif 'error' in result: # Fetch error / SPV finished validating the XML and sends back a disapproval answer
to_delete_documents |= invoice._l10n_ro_edi_get_sent_and_failed_documents()
result['key_loading'] = invoice.l10n_ro_edi_index
result['attachment_raw'] = previous_raw
invoice._l10n_ro_edi_create_document_invoice_sending_failed(result)
else: # result == {'key_download': <str>}; SPV finished validation and sends us an approval answer
# use the obtained key_download to immediately make a download request and process them
final_result = self.env['l10n_ro_edi.document']._request_ciusro_download_answer(
company=invoice.company_id,
key_download=result['key_download'],
session=session,
)
to_delete_documents |= invoice._l10n_ro_edi_get_sent_and_failed_documents()
final_result['key_loading'] = invoice.l10n_ro_edi_index
if 'error' in final_result:
final_result['attachment_raw'] = previous_raw
invoice._l10n_ro_edi_create_document_invoice_sending_failed(final_result)
else:
invoice._l10n_ro_edi_create_document_invoice_validated(final_result)
if not tools.config['test_enable'] and not modules.module.current_test:
self._cr.commit()
# Delete outdated documents in batches
to_delete_documents.unlink()