[IMP] account_edi: added error_level + better management of Lock on account.move

- Some errors will never get fixed until user intervention, it doesn't make sense to run the CRON when there is such error.
+ some small improvements and esthetic changes
- When we are in a CRON, we need to commit the changes between each call to web-service to avoid loss of data.
- Small refactor of edi.document prepare_jobs and process_jobs
- Added an arbitrary key to create the batches.
This commit is contained in:
Benjamin Frantzen (bfr)
2021-02-01 14:41:58 +00:00
parent c0738b8999
commit ce674f1b21
20 changed files with 887 additions and 116 deletions
+5
View File
@@ -308,6 +308,11 @@ msgstr ""
msgid "State"
msgstr ""
#. module: account_edi
#: model:ir.model.fields,help:account_edi_extended.field_account_edi_document__error
msgid "The text of the last error that happened during Electronic Invoice operation."
msgstr ""
#. module: account_edi
#: model:ir.model.fields,help:account_edi.field_account_bank_statement_line__edi_web_services_to_process
#: model:ir.model.fields,help:account_edi.field_account_move__edi_web_services_to_process
+136 -90
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo import models, fields, api
from odoo.addons.account_edi_extended.models.account_edi_document import DEFAULT_BLOCKING_LEVEL
from psycopg2 import OperationalError
import logging
@@ -17,7 +18,7 @@ class AccountEdiDocument(models.Model):
edi_format_id = fields.Many2one('account.edi.format', required=True)
attachment_id = fields.Many2one('ir.attachment', help='The file generated by edi_format_id when the invoice is posted (and this document is processed).')
state = fields.Selection([('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')])
error = fields.Html()
error = fields.Html(help='The text of the last error that happened during Electronic Invoice operation.')
# == Not stored fields ==
name = fields.Char(related='attachment_id.name')
@@ -31,24 +32,32 @@ class AccountEdiDocument(models.Model):
),
]
def write(self, vals):
''' If account_edi_extended is not installed, a default behaviour is used instead.
'''
if 'blocking_level' in vals and 'blocking_level' not in self.env['account.edi.document']._fields:
vals.pop('blocking_level')
return super().write(vals)
def _prepare_jobs(self):
"""Creates a list of jobs to be performed by '_process_jobs' for the documents in self.
"""Creates a list of jobs to be performed by '_process_job' for the documents in self.
Each document represent a job, BUT if multiple documents have the same state, edi_format_id,
doc_type (invoice or payment) and company_id AND the edi_format_id supports batching, they are grouped
into a single job.
:returns: A list of tuples (key, documents)
* key: A tuple (edi_format_id, state, doc_type, company_id)
** edi_format_id: The format to perform the operation with
** state: The state of the documents of this job
** doc_type: Are the moves of this job invoice or payments ?
** company_id: The company the moves belong to
* documents: The documents related to this job. If edi_format_id does not support batch, length must be one
:returns: A list of tuples (documents, doc_type)
* documents: The documents related to this job. If edi_format_id does not support batch, length is one
* doc_type: Are the moves of this job invoice or payments ?
"""
to_process = []
batches = {}
for edi_doc in self.filtered(lambda d: d.state in ('to_send', 'to_cancel')):
# Classify jobs by (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
to_process = {}
if 'blocking_level' in self.env['account.edi.document']._fields:
documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel') and d.blocking_level != 'error')
else:
documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel'))
for edi_doc in documents:
move = edi_doc.move_id
edi_format = edi_doc.edi_format_id
if move.is_invoice(include_receipts=True):
@@ -58,20 +67,64 @@ class AccountEdiDocument(models.Model):
else:
continue
key = (edi_format, edi_doc.state, doc_type, move.company_id)
if edi_format._support_batching():
if not batches.get(key, None):
batches[key] = self.env['account.edi.document']
batches[key] |= edi_doc
else:
to_process.append((key, edi_doc))
to_process.extend(batches.items())
return to_process
custom_key = edi_format._get_batch_key(edi_doc.move_id, edi_doc.state)
key = (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
to_process.setdefault(key, self.env['account.edi.document'])
to_process[key] |= edi_doc
# Order payments/invoice and create batches.
invoices = []
payments = []
for key, documents in to_process.items():
edi_format, state, doc_type, company_id, custom_key = key
target = invoices if doc_type == 'invoice' else payments
batch = self.env['account.edi.document']
for doc in documents:
if edi_format._support_batching(move=doc.move_id, state=state, company=company_id):
batch |= doc
else:
target.append((doc, doc_type))
if batch:
target.append((batch, doc_type))
return invoices + payments
@api.model
def _convert_to_old_jobs_format(self, jobs):
""" See '_prepare_jobs' :
Old format : ((edi_format, state, doc_type, company_id), documents)
Since edi_format, state and company_id can be deduced from documents, this is redundant and more prone to unexpected behaviours.
New format : (doc_type, documents).
However, for backward compatibility of 'process_jobs', we need a way to convert back to the old format.
"""
return [(
(documents.edi_format_id, documents[0].state, doc_type, documents.move_id.company_id),
documents
) for documents, doc_type in jobs]
@api.model
def _process_jobs(self, to_process):
""" Deprecated, use _process_job instead.
:param to_process: A list of tuples (key, documents)
* key: A tuple (edi_format_id, state, doc_type, company_id)
** edi_format_id: The format to perform the operation with
** state: The state of the documents of this job
** doc_type: Are the moves of this job invoice or payments ?
** company_id: The company the moves belong to
* documents: The documents related to this job. If edi_format_id does not support batch, length is one
"""
for key, documents in to_process:
dummy, dummy, doc_type, dummy = key
self._process_job(documents, doc_type)
@api.model
def _process_job(self, documents, doc_type):
"""Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
Invoices are processed before payments.
:param documents: The documents related to this job. If edi_format_id does not support batch, length is one
:param doc_type: Are the moves of this job invoice or payments ?
"""
def _postprocess_post_edi_results(documents, edi_result):
attachments_to_unlink = self.env['ir.attachment']
@@ -83,6 +136,7 @@ class AccountEdiDocument(models.Model):
values = {
'attachment_id': move_result['attachment'].id,
'error': move_result.get('error', False),
'blocking_level': move_result.get('blocking_level', DEFAULT_BLOCKING_LEVEL) if 'error' in move_result else False,
}
if not values.get('error'):
values.update({'state': 'sent'})
@@ -90,7 +144,10 @@ class AccountEdiDocument(models.Model):
if not old_attachment.res_model or not old_attachment.res_id:
attachments_to_unlink |= old_attachment
else:
document.error = move_result.get('error', _("Error when processing the journal entry."))
document.write({
'error': move_result.get('error', False),
'blocking_level': move_result.get('blocking_level', DEFAULT_BLOCKING_LEVEL) if 'error' in move_result else False,
})
# Attachments that are not explicitly linked to a business model could be removed because they are not
# supposed to have any traceability from the user.
@@ -102,12 +159,13 @@ class AccountEdiDocument(models.Model):
for document in documents:
move = document.move_id
move_result = edi_result.get(move, {})
if move_result.get('success'):
if move_result.get('success') is True:
old_attachment = document.attachment_id
document.write({
'state': 'cancelled',
'error': False,
'attachment_id': False,
'blocking_level': False,
})
if move.is_invoice(include_receipts=True) and move.state == 'posted':
@@ -118,8 +176,11 @@ class AccountEdiDocument(models.Model):
if not old_attachment.res_model or not old_attachment.res_id:
attachments_to_unlink |= old_attachment
else:
document.error = move_result.get('error') or _("Error when cancelling the journal entry.")
elif not move_result.get('success'):
document.write({
'error': move_result.get('error', False),
'blocking_level': move_result.get('blocking_level', DEFAULT_BLOCKING_LEVEL) if move_result.get('error') else False,
})
if invoice_ids_to_cancel:
invoices = self.env['account.move'].browse(list(invoice_ids_to_cancel))
@@ -132,80 +193,65 @@ class AccountEdiDocument(models.Model):
test_mode = self._context.get('edi_test_mode', False)
# ==== Process invoices ====
payments = []
for key, batches in to_process:
edi_format, state, doc_type, company_id = key
if doc_type == 'payment':
payments.append((key, batches))
continue # payments are processed after invoices
documents.edi_format_id.ensure_one() # All account.edi.document of a job should have the same edi_format_id
documents.move_id.company_id.ensure_one() # All account.edi.document of a job should be from the same company
if len(set(doc.state for doc in documents)) != 1:
raise ValueError('All account.edi.document of a job should have the same state')
for documents in batches:
move_to_cancel = documents.filtered(lambda doc: doc.edi_format_id._needs_web_services() \
and doc.attachment_id \
and doc.state == 'to_cancel' \
and doc.move_id.is_invoice(include_receipts=True) \
and doc.edi_format_id._is_required_for_invoice(doc.move_id)).move_id
attachments_potential_unlink = documents.attachment_id.filtered(lambda a: not a.res_model and not a.res_id)
edi_format = documents.edi_format_id
state = documents[0].state
if doc_type == 'invoice':
if state == 'to_send':
edi_result = edi_format._post_invoice_edi(documents.move_id, test_mode=test_mode)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = edi_format._cancel_invoice_edi(documents.move_id, test_mode=test_mode)
_postprocess_cancel_edi_results(documents, edi_result)
try:
with self.env.cr.savepoint(flush=False):
# Locks the documents in DB. Avoid sending an invoice twice (the documents can be processed by the CRON but also manually).
self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(documents.ids)])
# Locks the move that will be cancelled.
if move_to_cancel:
self._cr.execute('SELECT * FROM account_move WHERE id IN %s FOR UPDATE NOWAIT', [tuple(move_to_cancel.ids)])
# Locks the attachments that might be unlinked
if attachments_potential_unlink:
self._cr.execute('SELECT * FROM ir_attachment WHERE id IN %s FOR UPDATE NOWAIT', [tuple(attachments_potential_unlink.ids)])
if state == 'to_send':
edi_result = edi_format._post_invoice_edi(documents.move_id, test_mode=test_mode)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = edi_format._cancel_invoice_edi(documents.move_id, test_mode=test_mode)
_postprocess_cancel_edi_results(documents, edi_result)
except OperationalError as e:
if e.pgcode == '55P03':
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
else:
raise e
# ==== Process payments ====
for key, batches in payments:
edi_format, state, doc_type, company_id = key
for documents in batches:
try:
with self.env.cr.savepoint(flush=False):
self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)])
if state == 'to_send':
edi_result = edi_format._post_payment_edi(documents.move_id, test_mode=test_mode)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = edi_format._cancel_payment_edi(documents.move_id, test_mode=test_mode)
_postprocess_cancel_edi_results(documents, edi_result)
except OperationalError as e:
if e.pgcode == '55P03':
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
else:
raise e
elif doc_type == 'payment':
if state == 'to_send':
edi_result = edi_format._post_payment_edi(documents.move_id, test_mode=test_mode)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = edi_format._cancel_payment_edi(documents.move_id, test_mode=test_mode)
_postprocess_cancel_edi_results(documents, edi_result)
def _process_documents_no_web_services(self):
""" Post and cancel all the documents that don't need a web service.
"""
jobs = self.filtered(lambda d: not d.edi_format_id._needs_web_services())._prepare_jobs()
self._process_jobs(jobs)
self._process_jobs(self._convert_to_old_jobs_format(jobs))
def _process_documents_web_services(self, job_count=None):
def _process_documents_web_services(self, job_count=None, with_commit=True):
""" Post and cancel all the documents that need a web service. This is called by CRON.
:param job_count: Limit to the number of jobs to process among the ones that are available for treatment.
"""
jobs = self.filtered(lambda d: d.edi_format_id._needs_web_services())._prepare_jobs()
self._process_jobs(jobs[0:job_count or len(jobs)])
jobs = jobs[0:job_count or len(jobs)]
for documents, doc_type in jobs:
move_to_cancel = documents.filtered(lambda doc: doc.attachment_id \
and doc.state == 'to_cancel' \
and doc.move_id.is_invoice(include_receipts=True) \
and doc.edi_format_id._is_required_for_invoice(doc.move_id)).move_id
attachments_potential_unlink = documents.attachment_id.filtered(lambda a: not a.res_model and not a.res_id)
try:
with self.env.cr.savepoint(flush=False):
self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(documents.ids)])
# Locks the move that will be cancelled.
if move_to_cancel:
self._cr.execute('SELECT * FROM account_move WHERE id IN %s FOR UPDATE NOWAIT', [tuple(move_to_cancel.ids)])
# Locks the attachments that might be unlinked
if attachments_potential_unlink:
self._cr.execute('SELECT * FROM ir_attachment WHERE id IN %s FOR UPDATE NOWAIT', [tuple(attachments_potential_unlink.ids)])
self._process_job(documents, doc_type)
except OperationalError as e:
if e.pgcode == '55P03':
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
else:
raise e
else:
if with_commit and len(jobs) > 1:
self.env.cr.commit()
@@ -98,7 +98,7 @@ class AccountEdiFormat(models.Model):
# TO OVERRIDE
return False
def _support_batching(self):
def _support_batching(self, move=None, state=None, company=None):
""" Indicate if we can send multiple documents in the same time to the web services.
If True, the _post_%s_edi methods will get multiple documents in the same time.
Otherwise, these methods will be called with only one record at a time.
@@ -108,6 +108,26 @@ class AccountEdiFormat(models.Model):
# TO OVERRIDE
return False
def _get_batch_key(self, move, state):
""" Returns a tuple that will be used as key to partitionnate the invoices/payments when creating batches
with multiple invoices/payments.
The type of move (invoice or payment), its company_id, its edi state and the edi_format are used by default, if
no further partition is needed for this format, this method should return ().
:returns: The key to be used when partitionning the batches.
"""
move.ensure_one()
return ()
def _check_move_configuration(self, move):
""" Checks the move and relevant records for potential error (missing data, etc).
:param invoice: The move to check.
:returns: A list of error messages.
"""
# TO OVERRIDE
return []
def _post_invoice_edi(self, invoices, test_mode=False):
""" Create the file content representing the invoice (and calls web services if necessary).
@@ -116,6 +136,7 @@ class AccountEdiFormat(models.Model):
:returns: A dictionary with the invoice as key and as value, another dictionary:
* attachment: The attachment representing the invoice in this edi_format if the edi was successfully posted.
* error: An error if the edi was not successfully posted.
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -129,6 +150,7 @@ class AccountEdiFormat(models.Model):
:returns: A dictionary with the invoice as key and as value, another dictionary:
* success: True if the invoice was successfully cancelled.
* error: An error if the edi was not successfully cancelled.
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -142,6 +164,7 @@ class AccountEdiFormat(models.Model):
:returns: A dictionary with the payment as key and as value, another dictionary:
* attachment: The attachment representing the payment in this edi_format if the edi was successfully posted.
* error: An error if the edi was not successfully posted.
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -155,6 +178,7 @@ class AccountEdiFormat(models.Model):
:returns: A dictionary with the payment as key and as value, another dictionary:
* success: True if the payment was successfully cancelled.
* error: An error if the edi was not successfully cancelled.
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -224,7 +248,7 @@ class AccountEdiFormat(models.Model):
"""
attachments = []
for edi_format in self:
attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format).attachment_id
attachment = invoice._get_edi_attachment(edi_format)
if attachment and edi_format._is_embedding_to_invoice_pdf_needed():
datas = base64.b64decode(attachment.with_context(bin_size=False).datas)
attachments.append({'name': attachment.name, 'datas': datas})
@@ -419,7 +443,7 @@ class AccountEdiFormat(models.Model):
:param name: The name of the product.
:param default_code: The default_code of the product.
:param bracode: The barcode of the product.
:param barcode: The barcode of the product.
:returns: A product or an empty recordset if not found.
'''
domains = []
@@ -455,3 +479,12 @@ class AccountEdiFormat(models.Model):
:returns: A currency or an empty recordset if not found.
'''
return self.env['res.currency'].search([('name', '=', code.upper())], limit=1)
####################################################
# Other helpers
####################################################
@api.model
def _format_error_message(self, error_title, errors):
bullet_list_msg = ''.join('<li>%s</li>' % msg for msg in errors)
return '%s<ul>%s</ul>' % (error_title, bullet_list_msg)
+22 -6
View File
@@ -111,6 +111,7 @@ class AccountMove(models.Model):
existing_edi_document.write({
'state': 'to_send',
'error': False,
'blocking_level': False,
})
else:
edi_document_vals_list.append({
@@ -122,6 +123,7 @@ class AccountMove(models.Model):
existing_edi_document.write({
'state': False,
'error': False,
'blocking_level': False,
})
self.env['account.edi.document'].create(edi_document_vals_list)
@@ -138,6 +140,10 @@ class AccountMove(models.Model):
is_edi_needed = move.is_invoice(include_receipts=False) and edi_format._is_required_for_invoice(move)
if is_edi_needed:
errors = edi_format._check_move_configuration(move)
if errors:
raise UserError(_("Invalid invoice configuration:\n\n%s") % '\n'.join(errors))
existing_edi_document = move.edi_document_ids.filtered(lambda x: x.edi_format_id == edi_format)
if existing_edi_document:
existing_edi_document.write({
@@ -160,8 +166,8 @@ class AccountMove(models.Model):
# Set the electronic document to be canceled and cancel immediately for synchronous formats.
res = super().button_cancel()
self.edi_document_ids.filtered(lambda doc: doc.attachment_id).write({'state': 'to_cancel', 'error': False})
self.edi_document_ids.filtered(lambda doc: not doc.attachment_id).write({'state': 'cancelled', 'error': False})
self.edi_document_ids.filtered(lambda doc: doc.attachment_id).write({'state': 'to_cancel', 'error': False, 'blocking_level': False})
self.edi_document_ids.filtered(lambda doc: not doc.attachment_id).write({'state': 'cancelled', 'error': False, 'blocking_level': False})
self.edi_document_ids._process_documents_no_web_services()
return res
@@ -177,7 +183,7 @@ class AccountMove(models.Model):
res = super().button_draft()
self.edi_document_ids.write({'state': False, 'error': False})
self.edi_document_ids.write({'state': False, 'error': False, 'blocking_level': False})
return res
@@ -198,7 +204,13 @@ class AccountMove(models.Model):
if is_move_marked:
move.message_post(body=_("A cancellation of the EDI has been requested."))
to_cancel_documents.write({'state': 'to_cancel', 'error': False})
to_cancel_documents.write({'state': 'to_cancel', 'error': False, 'blocking_level': False})
def _get_edi_document(self, edi_format):
return self.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format)
def _get_edi_attachment(self, edi_format):
return self._get_edi_document(edi_format).attachment_id
####################################################
# Import Electronic Document
@@ -242,8 +254,12 @@ class AccountMove(models.Model):
####################################################
def action_process_edi_web_services(self):
self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel'))._process_documents_web_services()
docs = self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel'))
if 'blocking_level' in self.env['account.edi.document']._fields:
docs = docs.filtered(lambda d: d.blocking_level != 'error')
else:
docs = docs.filtered(lambda d: not d.error)
docs._process_documents_web_services(with_commit=False)
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
+21 -5
View File
@@ -32,7 +32,7 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
####################################################
def edi_cron(self):
self.env['account.edi.document'].sudo().with_context(edi_test_mode=True).search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services()
self.env['account.edi.document'].sudo().with_context(edi_test_mode=True).search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services(with_commit=False)
def _create_empty_vendor_bill(self):
invoice = self.env['account.move'].create({
@@ -71,8 +71,10 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
def assert_generated_file_equal(self, invoice, expected_values, applied_xpath=None):
invoice.action_post()
invoice.edi_document_ids._process_documents_web_services() # synchronous are called in post, but there's no CRON in tests for asynchronous
attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == self.edi_format).attachment_id
invoice.edi_document_ids._process_documents_web_services(with_commit=False) # synchronous are called in post, but there's no CRON in tests for asynchronous
attachment = invoice._get_edi_attachment(self.edi_format)
if not attachment:
raise ValueError('No attachment was generated after posting EDI')
xml_content = base64.b64decode(attachment.with_context(bin_size=False).datas)
current_etree = self.get_xml_tree_from_string(xml_content)
expected_etree = self.get_xml_tree_from_string(expected_values)
@@ -80,20 +82,34 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
expected_etree = self.with_applied_xpath(expected_etree, applied_xpath)
self.assertXmlTreeEqual(current_etree, expected_etree)
def create_edi_document(self, edi_format, state, move=None, move_type=None):
""" Creates a document based on an existing invoice or creates one, too.
:param edi_format: The edi_format of the document.
:param state: The state of the document.
:param move: The move of the document or None to create a new one.
:param move_type: If move is None, the type of the invoice to create, defaults to 'out_invoice'.
"""
move = move or self.init_invoice(move_type or 'out_invoice')
return self.env['account.edi.document'].create({
'edi_format_id': edi_format.id,
'move_id': move.id,
'state': state
})
def _process_documents_web_services(self, moves, formats_to_return=None):
""" Generates and returns EDI files for the specified moves.
formats_to_return is an optional parameter used to pass a set of codes from
the formats we want to return the files for (in case we want to test specific formats).
Other formats will still generate documents, they simply won't be returned.
"""
moves.edi_document_ids.with_context(edi_test_mode=True)._process_documents_web_services()
moves.edi_document_ids.with_context(edi_test_mode=True)._process_documents_web_services(with_commit=False)
documents_to_return = moves.edi_document_ids
if formats_to_return != None:
documents_to_return = documents_to_return.filtered(lambda x: x.edi_format_id.code in formats_to_return)
attachments = documents_to_return.attachment_id
data_str_list = []
for attachment in attachments.with_context(bin_size=False):
data_str_list.append(base64.decodebytes(attachment.datas))
+4 -11
View File
@@ -14,17 +14,10 @@ class TestAccountEdi(AccountEdiTestCommon):
self.assertEqual(len(invoice.edi_document_ids), 1)
def test_prepare_jobs(self):
def create_edi_document(edi_format, state, move=None, move_type=None):
move = move or self.init_invoice(move_type or 'out_invoice')
return self.env['account.edi.document'].create({
'edi_format_id': edi_format.id,
'move_id': move.id,
'state': state
})
edi_docs = self.env['account.edi.document']
edi_docs |= create_edi_document(self.edi_format, 'to_send')
edi_docs |= create_edi_document(self.edi_format, 'to_send')
edi_docs |= self.create_edi_document(self.edi_format, 'to_send')
edi_docs |= self.create_edi_document(self.edi_format, 'to_send')
to_process = edi_docs._prepare_jobs()
self.assertEqual(len(to_process), 2)
@@ -38,8 +31,8 @@ class TestAccountEdi(AccountEdiTestCommon):
'code': 'test_batch_edi_2',
})
edi_docs |= create_edi_document(other_edi, 'to_send')
edi_docs |= create_edi_document(other_edi, 'to_send')
edi_docs |= self.create_edi_document(other_edi, 'to_send')
edi_docs |= self.create_edi_document(other_edi, 'to_send')
with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching', return_value=True):
to_process = edi_docs._prepare_jobs()
@@ -24,7 +24,7 @@
</xpath>
<xpath expr="//header" position="after">
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('edi_web_services_to_process', 'in', ['', False])]}">
attrs="{'invisible': ['|', ('edi_web_services_to_process', 'in', ['', False]), ('state', '=', 'draft')]}">
<div>The invoice will be sent asynchronously to :
<field name="edi_web_services_to_process" class="oe_inline"/>
</div>
+13
View File
@@ -0,0 +1,13 @@
# -*- encoding: utf-8 -*-
from . import models
from odoo import api, SUPERUSER_ID
def account_edi_block_level(cr, registery):
''' The default value for blocking_level is 'error', but without this module,
the behavior is the same as a blocking_level of 'warning' so we need to set
all documents in error.
'''
env = api.Environment(cr, SUPERUSER_ID, {})
env['account.edi.document'].search([('error', '!=', False)]).write({'blocking_level': 'warning'})
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name' : 'Additionnal features for account_edi',
'description':"""
This module add features to account_edi to support new Edi formats.
""",
'version' : '1.0',
'category': 'Accounting/Accounting',
'depends' : ['account_edi'],
'data': [
'views/account_edi_document_views.xml',
'views/account_move_views.xml',
'views/account_payment_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'post_init_hook': 'account_edi_block_level',
}
@@ -0,0 +1,151 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_edi_extended
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-05 11:07+0000\n"
"PO-Revision-Date: 2021-01-05 11:07+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid " Electronic invoicing error(s)"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid " Electronic invoicing info(s)"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid " Electronic invoicing warning(s)"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid "A request for cancellation of the EDI has been called off."
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document__blocking_level
msgid "Blocking Level"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,help:account_edi_extended.field_account_edi_document__blocking_level
msgid ""
"Blocks the document current operation depending on the error severity :\n"
" * Info: the document is not blocked and everything is working as it should.\n"
" * Warning : there is an error that doesn't prevent the current Electronic Invoicing operation to succeed.\n"
" * Error : there is an error that blocks the current Electronic Invoicing operation."
msgstr ""
#. module: account_edi_extended
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_move_form_inherit
msgid "Call off EDI Cancellation"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document__display_name
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__display_name
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__display_name
msgid "Display Name"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_bank_statement_line__edi_blocking_level
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__edi_blocking_level
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__edi_blocking_level
msgid "Edi Blocking Level"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_bank_statement_line__edi_error_message
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__edi_error_message
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__edi_error_message
msgid "Edi Error Message"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_bank_statement_line__edi_show_abandon_cancel_button
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__edi_show_abandon_cancel_button
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__edi_show_abandon_cancel_button
msgid "Edi Show Abandon Cancel Button"
msgstr ""
#. module: account_edi_extended
#: model:ir.model,name:account_edi_extended.model_account_edi_document
msgid "Electronic Document for an account.move"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_edi_document__blocking_level__error
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_move__edi_blocking_level__error
msgid "Error"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document__id
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__id
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__id
msgid "ID"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_edi_document__blocking_level__info
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_move__edi_blocking_level__info
msgid "Info"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_edi_document.py:0
#, python-format
msgid "Invalid configuration:"
msgstr ""
#. module: account_edi_extended
#: model:ir.model,name:account_edi_extended.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document____last_update
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move____last_update
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment____last_update
msgid "Last Modified on"
msgstr ""
#. module: account_edi_extended
#: model:ir.model,name:account_edi_extended.model_account_payment
msgid "Payments"
msgstr ""
#. module: account_edi_extended
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_move_form_inherit
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_payment_form_inherit
msgid "Retry"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_edi_document__blocking_level__warning
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_move__edi_blocking_level__warning
msgid "Warning"
msgstr ""
#. module: account_edi_extended
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_move_form_inherit
msgid "⇒ See errors"
msgstr ""
@@ -0,0 +1,3 @@
from . import account_edi_document
from . import account_move
from . import account_payment
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
import logging
_logger = logging.getLogger(__name__)
DEFAULT_BLOCKING_LEVEL = 'warning' # Keep previous behavior. TODO : when account_edi_extended is merged with account_edi, should be 'error' (document will not be processed again until forced retry or reset to draft)
class AccountEdiDocument(models.Model):
_inherit = 'account.edi.document'
blocking_level = fields.Selection(selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')],
help="Blocks the document current operation depending on the error severity :\n"
" * Info: the document is not blocked and everything is working as it should.\n"
" * Warning : there is an error that doesn't prevent the current Electronic Invoicing operation to succeed.\n"
" * Error : there is an error that blocks the current Electronic Invoicing operation.")
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
class AccountMove(models.Model):
_inherit = 'account.move'
edi_show_abandon_cancel_button = fields.Boolean(
compute='_compute_edi_show_abandon_cancel_button')
edi_error_message = fields.Html(compute='_compute_edi_error_message')
edi_blocking_level = fields.Selection(selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')], compute='_compute_edi_error_message')
@api.depends(
'edi_document_ids',
'edi_document_ids.state',
'edi_document_ids.blocking_level',
'edi_document_ids.edi_format_id',
'edi_document_ids.edi_format_id.name')
def _compute_edi_web_services_to_process(self):
# OVERRIDE to take blocking_level into account
for move in self:
to_process = move.edi_document_ids.filtered(lambda d: d.state in ['to_send', 'to_cancel'] and d.blocking_level != 'error')
format_web_services = to_process.edi_format_id.filtered(lambda f: f._needs_web_services())
move.edi_web_services_to_process = ', '.join(f.name for f in format_web_services)
@api.depends(
'state',
'edi_document_ids.state',
'edi_document_ids.attachment_id')
def _compute_edi_show_abandon_cancel_button(self):
for move in self:
move.edi_show_abandon_cancel_button = any(doc.edi_format_id._needs_web_services()
and doc.state == 'to_cancel'
and move.is_invoice(include_receipts=True)
and doc.edi_format_id._is_required_for_invoice(move)
for doc in move.edi_document_ids)
@api.depends('edi_error_count', 'edi_document_ids.error', 'edi_document_ids.blocking_level')
def _compute_edi_error_message(self):
for move in self:
if move.edi_error_count == 0:
move.edi_error_message = None
move.edi_blocking_level = None
elif move.edi_error_count == 1:
error_doc = move.edi_document_ids.filtered(lambda d: d.error)
move.edi_error_message = error_doc.error
move.edi_blocking_level = error_doc.blocking_level
else:
error_levels = set([doc.blocking_level for doc in move.edi_document_ids])
if 'error' in error_levels:
move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing error(s)")
move.edi_blocking_level = 'error'
elif 'warning' in error_levels:
move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing warning(s)")
move.edi_blocking_level = 'warning'
else:
move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing info(s)")
move.edi_blocking_level = 'info'
def action_retry_edi_documents_error(self):
self.edi_document_ids.write({'error': False, 'blocking_level': False})
self.action_process_edi_web_services()
def button_abandon_cancel_posted_posted_moves(self):
'''Cancel the request for cancellation of the EDI.
'''
documents = self.env['account.edi.document']
for move in self:
is_move_marked = False
for doc in move.edi_document_ids:
if doc.state == 'to_cancel' \
and move.is_invoice(include_receipts=True) \
and doc.edi_format_id._is_required_for_invoice(move):
documents |= doc
is_move_marked = True
if is_move_marked:
move.message_post(body=_("A request for cancellation of the EDI has been called off."))
documents.write({'state': 'sent'})
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountPayment(models.Model):
_inherit = 'account.payment'
def action_retry_edi_documents_error(self):
self.ensure_one()
return self.move_id.action_retry_edi_documents_error()
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_edi
+114
View File
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from contextlib import contextmanager
from unittest.mock import patch
import base64
# TODO for the test to work, we need a chart template (COA) but we don't have any and don't want to add dependency (create empty coa ?)
def _generate_mocked_needs_web_services(needs_web_services):
return lambda edi_format: needs_web_services
def _generate_mocked_support_batching(support_batching):
return lambda edi_format, move, state, company: support_batching
def _mocked_get_batch_key(edi_format, move, state):
return ()
def _mocked_check_move_configuration_success(edi_format, move):
return []
def _mocked_check_move_configuration_fail(edi_format, move):
return ['Fake error (mocked)']
def _mocked_post(edi_format, invoices, test_mode):
res = {}
for invoice in invoices:
attachment = edi_format.env['ir.attachment'].create({
'name': 'mock_simple.xml',
'datas': base64.encodebytes(b"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
'mimetype': 'application/xml'
})
res[invoice] = {'attachment': attachment}
return res
def _mocked_post_two_steps(edi_format, invoices, test_mode):
# For this test, we use the field ref to know if the first step is already done or not.
# Typically, a technical field for the reference of the upload to the web-service will
# be saved on the invoice.
invoices_no_ref = invoices.filtered(lambda i: not i.ref)
if len(invoices_no_ref) == len(invoices): # first step
invoices_no_ref.ref = 'test_ref'
return {invoice: {} for invoice in invoices}
elif len(invoices_no_ref) == 0: # second step
res = {}
for invoice in invoices:
attachment = edi_format.env['ir.attachment'].create({
'name': 'mock_simple.xml',
'datas': base64.encodebytes(b"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
'mimetype': 'application/xml'
})
res[invoice] = {'attachment': attachment}
return res
else:
raise ValueError('wrong use of "_mocked_post_two_steps"')
def _mocked_cancel_success(edi_format, invoices, test_mode):
return {invoice: {'success': True} for invoice in invoices}
def _mocked_cancel_failed(edi_format, invoices, test_mode):
return {invoice: {'error': 'Faked error (mocked)'} for invoice in invoices}
class AccountEdiExtendedTestCommon(AccountEdiTestCommon):
@contextmanager
def mock_edi(self,
_is_required_for_invoice_method=lambda edi_format, invoice: True,
_is_required_for_payment_method=lambda edi_format, invoice: True,
_support_batching_method=_generate_mocked_support_batching(False),
_get_batch_key_method=_mocked_get_batch_key,
_needs_web_services_method=_generate_mocked_needs_web_services(False),
_check_move_configuration_method=_mocked_check_move_configuration_success,
_post_invoice_edi_method=_mocked_post,
_cancel_invoice_edi_method=_mocked_cancel_success,
_post_payment_edi_method=_mocked_post,
_cancel_payment_edi_method=_mocked_cancel_success,
):
try:
with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._is_required_for_invoice',
new=_is_required_for_invoice_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._is_required_for_payment',
new=_is_required_for_payment_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
new=_needs_web_services_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching',
new=_support_batching_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._get_batch_key',
new=_get_batch_key_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._check_move_configuration',
new=_check_move_configuration_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_invoice_edi',
new=_post_invoice_edi_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._cancel_invoice_edi',
new=_cancel_invoice_edi_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_payment_edi',
new=_post_payment_edi_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._cancel_payment_edi',
new=_cancel_payment_edi_method):
yield
finally:
pass
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account_edi_extended.tests.common import AccountEdiExtendedTestCommon, _mocked_post_two_steps, _generate_mocked_needs_web_services, _mocked_cancel_failed, _generate_mocked_support_batching
class TestAccountEdi(AccountEdiExtendedTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None, edi_format_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a + cls.product_b)
def test_edi_flow(self):
with self.mock_edi():
doc = self.invoice._get_edi_document(self.edi_format)
self.assertFalse(doc)
self.invoice.action_post()
doc = self.invoice._get_edi_document(self.edi_format)
self.assertEqual(len(doc), 1)
self.assertEqual(doc.state, 'sent')
self.invoice.button_draft()
self.invoice.button_cancel()
self.assertEqual(doc.state, 'cancelled')
def test_edi_flow_two_steps(self):
with self.mock_edi(_post_invoice_edi_method=_mocked_post_two_steps,
_needs_web_services_method=_generate_mocked_needs_web_services(True)):
doc = self.invoice._get_edi_document(self.edi_format)
self.assertFalse(doc)
self.invoice.action_post()
doc = self.invoice._get_edi_document(self.edi_format)
self.assertEqual(len(doc), 1)
self.assertEqual(doc.state, 'to_send')
doc._process_documents_web_services(with_commit=False)
self.assertEqual(doc.state, 'to_send')
doc._process_documents_web_services(with_commit=False)
self.assertEqual(doc.state, 'sent')
def test_edi_flow_request_cancel_success(self):
with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True)):
self.assertEqual(self.invoice.state, 'draft')
self.invoice.action_post()
doc = self.invoice._get_edi_document(self.edi_format)
self.assertEqual(doc.state, 'to_send')
self.assertEqual(self.invoice.state, 'posted')
doc._process_documents_web_services(with_commit=False)
self.assertEqual(doc.state, 'sent')
self.assertEqual(self.invoice.state, 'posted')
self.invoice.button_cancel_posted_moves()
self.assertEqual(doc.state, 'to_cancel')
self.assertEqual(self.invoice.state, 'posted')
doc._process_documents_web_services()
self.assertEqual(doc.state, 'cancelled')
self.assertEqual(self.invoice.state, 'cancel')
def test_edi_flow_request_cancel_failed(self):
with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True),
_cancel_invoice_edi_method=_mocked_cancel_failed):
self.assertEqual(self.invoice.state, 'draft')
self.invoice.action_post()
doc = self.invoice._get_edi_document(self.edi_format)
self.assertEqual(doc.state, 'to_send')
self.assertEqual(self.invoice.state, 'posted')
doc._process_documents_web_services(with_commit=False)
self.assertEqual(doc.state, 'sent')
self.assertEqual(self.invoice.state, 'posted')
self.invoice.button_cancel_posted_moves()
self.assertEqual(doc.state, 'to_cancel')
self.assertEqual(self.invoice.state, 'posted')
# Call off edi Cancellation
self.invoice.button_abandon_cancel_posted_posted_moves()
self.assertEqual(doc.state, 'sent')
self.assertFalse(doc.error)
# Failed cancel
self.invoice.button_cancel_posted_moves()
self.assertEqual(doc.state, 'to_cancel')
self.assertEqual(self.invoice.state, 'posted')
doc._process_documents_web_services()
self.assertEqual(doc.state, 'to_cancel')
self.assertEqual(self.invoice.state, 'posted')
# Call off edi Cancellation
self.invoice.button_abandon_cancel_posted_posted_moves()
self.assertEqual(doc.state, 'sent')
self.assertIsNotNone(doc.error)
def test_edi_flow_two_step_cancel_with_call_off_request(self):
def _mock_cancel(edi_format, invoices, test_mode):
invoices_no_ref = invoices.filtered(lambda i: not i.ref)
if len(invoices_no_ref) == len(invoices): # first step
invoices_no_ref.ref = 'test_ref_cancel'
return {invoice: {} for invoice in invoices}
elif len(invoices_no_ref) == 0: # second step
for invoice in invoices:
invoice.ref = None
return {invoice: {'success': True} for invoice in invoices}
else:
raise ValueError('wrong use of "_mocked_post_two_steps"')
def _is_needed_for_invoice(edi_format, invoice):
return not bool(invoice.ref)
with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True),
_is_required_for_invoice_method=_is_needed_for_invoice,
_cancel_invoice_edi_method=_mock_cancel):
self.invoice.action_post()
doc = self.invoice._get_edi_document(self.edi_format)
doc._process_documents_web_services(with_commit=False)
self.assertEqual(doc.state, 'sent')
# Request Cancellation
self.invoice.button_cancel_posted_moves()
doc._process_documents_web_services(with_commit=False) # first step of cancel
self.assertEqual(doc.state, 'to_cancel')
# Call off edi Cancellation
self.invoice.button_abandon_cancel_posted_posted_moves()
self.assertEqual(doc.state, 'to_cancel')
# If we cannot call off edi cancellation, only solution is to post again
doc._process_documents_web_services(with_commit=False) # second step of cancel
self.assertEqual(doc.state, 'cancelled')
self.invoice.action_post()
doc._process_documents_web_services(with_commit=False)
self.assertEqual(doc.state, 'sent')
def test_batches(self):
def _get_batch_key_method(edi_format, move, state):
return (move.ref)
with self.mock_edi(_get_batch_key_method=_get_batch_key_method,
_support_batching_method=_generate_mocked_support_batching(True)):
edi_docs = self.env['account.edi.document']
doc1 = self.create_edi_document(self.edi_format, 'to_send')
edi_docs |= doc1
doc2 = self.create_edi_document(self.edi_format, 'to_send')
edi_docs |= doc2
doc3 = self.create_edi_document(self.edi_format, 'to_send')
edi_docs |= doc3
to_process = edi_docs._prepare_jobs()
self.assertEqual(len(to_process), 1)
doc1.move_id.ref = 'batch1'
doc2.move_id.ref = 'batch2'
doc3.move_id.ref = 'batch3'
to_process = edi_docs._prepare_jobs()
self.assertEqual(len(to_process), 3)
doc2.move_id.ref = 'batch1'
to_process = edi_docs._prepare_jobs()
self.assertEqual(len(to_process), 2)
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_tree_account_edi_document_inherit" model="ir.ui.view">
<field name="name">Account.edi.document.tree.inherit</field>
<field name="model">account.edi.document</field>
<field name="inherit_id" ref="account_edi.view_tree_account_edi_document"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<field name="blocking_level" invisible="1" />
</xpath>
<xpath expr="//tree" position="attributes">
<attribute name="decoration-info">blocking_level == 'info'</attribute>
<attribute name="decoration-warning">blocking_level == 'warning'</attribute>
<attribute name="decoration-danger">blocking_level == 'error'</attribute>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_move_form_inherit" model="ir.ui.view">
<field name="name">account.move.form.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_move_form_inherit" />
<field name="arch" type="xml">
<xpath expr="//button[@name='%(account_edi.action_open_edi_documents)d']" position="after">
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</xpath>
<xpath expr="//button[@name='button_cancel_posted_moves']" position="after">
<field name="edi_show_abandon_cancel_button" invisible="1"/>
<button name="button_abandon_cancel_posted_posted_moves"
string="Call off EDI Cancellation"
type="object"
groups="account.group_account_invoice"
attrs="{'invisible' : [('edi_show_abandon_cancel_button', '=', False)]}"/>
</xpath>
<!-- Nasty xpath to replace the error count warning banner. In master, it will be merged. -->
<xpath expr="//div[hasclass('alert-warning')]" position="replace">
<field name="edi_blocking_level" invisible="1" />
<field name="edi_error_count" invisible="1" />
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'error')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'warning')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'info')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_payment_form_inherit" model="ir.ui.view">
<field name="name">account.payment.form.inherit</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account_edi.view_payment_form_inherit" />
<field name="arch" type="xml">
<xpath expr="//button[@name='%(account_edi.action_open_payment_edi_documents)d']" position="after">
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</xpath>
</field>
</record>
</data>
</odoo>