263 lines
13 KiB
Python
263 lines
13 KiB
Python
|
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()
|