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': , '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_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': , 'key_loading': , '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_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': , 'attachment_raw': } self._l10n_ro_edi_get_failed_documents().unlink() self._l10n_ro_edi_create_document_invoice_sending_failed(result) else: # result == {'key_loading': , 'attachment_raw': }; 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': }; 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()