Odoo18-Base/addons/l10n_my_edi/wizard/account_move_send.py

204 lines
9.3 KiB
Python
Raw Permalink Normal View History

2025-03-10 10:52:11 +07:00
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import time
from collections import defaultdict
from odoo import SUPERUSER_ID, _, api, fields, models
class AccountMoveSend(models.TransientModel):
_inherit = 'account.move.send'
l10n_my_edi_enable = fields.Boolean(
compute='_compute_l10n_my_edi_enable',
)
l10n_my_edi_send_checkbox = fields.Boolean(
compute='_compute_l10n_my_edi_send_checkbox',
string='Send to MyInvois',
readonly=False,
store=True,
)
def _get_wizard_values(self):
# EXTENDS 'account'
values = super()._get_wizard_values()
values['l10n_my_edi_send'] = self.l10n_my_edi_send_checkbox
return values
@api.depends('move_ids')
def _compute_l10n_my_edi_enable(self):
"""
E-invoicing is a legal requirement that doesn't require any special action from the user,
so we enable by default.
"""
# If there is no proxy user set and active, the feature shouldn't be available on invoices.
for wizard in self:
wizard.l10n_my_edi_enable = any(self._l10n_my_edi_need_edi(move) for move in wizard.move_ids)
@api.depends('l10n_my_edi_enable')
def _compute_l10n_my_edi_send_checkbox(self):
for wizard in self:
wizard.l10n_my_edi_send_checkbox = wizard.l10n_my_edi_enable
# -------------------------------------------------------------------------
# ATTACHMENTS
# -------------------------------------------------------------------------
@api.model
def _get_invoice_extra_attachments(self, move):
""" We are required to either:
- Attach a QR code to the invoice PDF, that points to the e-invoice on the MyInvois platform
- Attach the XML file we generated
We will use to later for simplicity. It is unclear if the shared xml should be digitally signed or not.
"""
# EXTENDS 'account'
return (
super()._get_invoice_extra_attachments(move)
+ move.l10n_my_edi_file_id
)
@api.depends('l10n_my_edi_send_checkbox')
def _compute_mail_attachments_widget(self):
# EXTENDS 'account' - add depends
super()._compute_mail_attachments_widget()
@api.model
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
# EXTENDS 'account'
super()._hook_invoice_document_before_pdf_report_render(invoice, invoice_data)
self._l10n_my_edi_generate_myinvois_xml(invoice, invoice_data)
@api.model
def _l10n_my_edi_generate_myinvois_xml(self, invoice, invoice_data):
# It should always be generated when sending.
if invoice_data.get('l10n_my_edi_send') or invoice_data.get('l10n_my_edi_generate'):
# We don't pre-check the configuration, the ubl export will handle that part.
xml_content, errors = invoice._l10n_my_edi_generate_invoice_xml()
if errors:
invoice_data['error'] = {
'error_title': _('Error when generating MyInvois file:'),
'errors': errors,
}
else:
invoice_data['myinvois_attachments'] = [{
'name': f'{invoice.name.replace("/", "_")}_myinvois.xml',
'raw': xml_content,
'mimetype': 'application/xml',
'res_model': invoice._name,
'res_id': invoice.id,
'res_field': 'l10n_my_edi_file', # Binary field
}]
@api.model
def _l10n_my_edi_need_edi(self, invoice, acceptable_states=None):
""" This will determine if a specific invoice is fit to interact with the API from here.
When submitting, we want to avoid any invoice that already have a state (which means they were submitted) but
when fetching the updated states, in_progress invoices are acceptable.
"""
return (invoice.is_invoice()
and invoice.state == 'posted'
and invoice.country_code == 'MY'
and (not invoice.l10n_my_edi_state or acceptable_states and invoice.l10n_my_edi_state in acceptable_states)
and invoice.company_id.l10n_my_edi_proxy_user_id)
@api.model
def _call_web_service_before_invoice_pdf_render(self, invoices_data):
# EXTENDS 'account'
super()._call_web_service_before_invoice_pdf_render(invoices_data)
xml_contents = defaultdict(list)
moves = self.env['account.move']
# This step is skipped if the move was sent, but not validated.
for move, move_data in invoices_data.items():
if not move_data.get('l10n_my_edi_send') or not self._l10n_my_edi_need_edi(move):
continue
moves |= move
if 'myinvois_attachments' in move_data:
xml_content = move_data['myinvois_attachments'][0]['raw'].decode('utf-8')
# If the invoice was downloaded but not sent, the json file could already be there.
elif move.l10n_my_edi_file:
xml_content = base64.b64decode(move.l10n_my_edi_file).decode('utf-8')
# If we don't have the file data and the file, we will regenerate it.
else:
self._l10n_my_edi_generate_myinvois_xml(move, move_data)
if 'myinvois_attachments' not in move_data:
continue # If an error occurred, it'll be in move_data['error'] so we can skip this invoice
xml_content = move_data['myinvois_attachments'][0]['raw'].decode('utf-8')
xml_contents[move] = xml_content
if moves and xml_contents:
errors = moves._l10n_my_edi_submit_documents(xml_contents)
if errors:
for move, move_data in invoices_data.items():
if move in errors:
move_data['error'] = {
'error_title': _('Error when sending the invoices to the E-invoicing service.'),
'errors': errors[move],
}
# Whatever happened, we need to commit once at this point, because another api call is done later on
# And in case of single invoice, a request error could raise => We would lose the uuid etc.
if self._can_commit():
self._cr.commit()
def _call_web_service_after_invoice_pdf_render(self, invoices_data):
"""
We need to get the submission status (valid, invalid) now for the flow to make sense.
If we follow their documentation, the status should be available near instantly (in 2s of the submission) which
means that it should be there in the time we get the submission response back and generate the invoice(s) pdf.
We will try up to three time with a 2s delay to make it happen. It should cover most of the use cases, as long as
there are no network issues/...
If after three time the invoice still has not been processed, we will move on and leave the update to the
scheduled action that fetches new incoming invoices and update statuses at the same time.
"""
# EXTENDS 'account'
super()._call_web_service_after_invoice_pdf_render(invoices_data)
moves = self.env['account.move']
for move, move_data in invoices_data.items():
if not move_data.get('l10n_my_edi_send') or not self._l10n_my_edi_need_edi(move, acceptable_states={'in_progress'}):
continue
moves |= move
if moves:
# This update can fail, but we don't consider that as a blocking error.
# If the api request fails (timeout, validation not finished, ...) it'll be retried in the cron `cron_name`.
invoices = self.env['account.move'].concat(*list(invoices_data.keys()))
retry = 0
errors, any_in_progress = invoices._l10n_my_edi_fetch_updated_statuses()
while any_in_progress and retry < 2:
time.sleep(1) # We wait a second before retrying.
errors, any_in_progress = invoices._l10n_my_edi_fetch_updated_statuses()
retry += 1
# While technically an in_progress status is not an error, it won't hurt much to display it as such.
# The "error" message in this case should be clear enough.
if errors:
for move, move_data in invoices_data.items():
if move in errors:
move_data['error'] = {
'error_title': _('Error when fetching statuses from the E-invoicing service.'),
'errors': errors[move],
}
# We commit again if possible, to ensure that the invoice status is set in the database in case of errors later.
if self._can_commit():
self._cr.commit()
@api.model
def _link_invoice_documents(self, invoice, invoice_data):
# EXTENDS 'account'
super()._link_invoice_documents(invoice, invoice_data)
attachment_vals = invoice_data.get('myinvois_attachments')
if attachment_vals:
self.env['ir.attachment'].with_user(SUPERUSER_ID).create(attachment_vals)
invoice.invalidate_recordset(fnames=['l10n_my_edi_file_id', 'l10n_my_edi_file'])