204 lines
9.3 KiB
Python
204 lines
9.3 KiB
Python
|
# 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'])
|