Skip to content

Commit

Permalink
[IMP] account_edi, l10n_*: Make the EDI engine more flexible
Browse files Browse the repository at this point in the history
Add a new method `_get_move_applicability` allowing to trigger the EDI on any journal entry, using the custom functions you want.

closes odoo#99227

Related: odoo/enterprise#30869
Signed-off-by: Laurent Smet <[email protected]>
  • Loading branch information
smetl authored and JulienVR committed Oct 3, 2022
1 parent 631d9c6 commit 0e5626c
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 686 deletions.
117 changes: 54 additions & 63 deletions addons/account_edi/models/account_edi_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ def _compute_edi_content(self):
config_errors = doc.edi_format_id._check_move_configuration(move)
if config_errors:
res = base64.b64encode('\n'.join(config_errors).encode('UTF-8'))
elif move.is_invoice(include_receipts=True) and doc.edi_format_id._is_required_for_invoice(move):
res = base64.b64encode(doc.edi_format_id._get_invoice_edi_content(doc.move_id))
elif move.payment_id and doc.edi_format_id._is_required_for_payment(move):
res = base64.b64encode(doc.edi_format_id._get_payment_edi_content(doc.move_id))
else:
move_applicability = doc.edi_format_id._get_move_applicability(move)
if move_applicability and move_applicability.get('edi_content'):
res = base64.b64encode(move_applicability['edi_content'](move))
doc.edi_content = res

def action_export_xml(self):
Expand All @@ -71,52 +71,44 @@ def _prepare_jobs(self):
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 (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 ?
:returns: [{
'documents': account.edi.document,
'method_to_call': str,
}]
"""

# Classify jobs by (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
to_process = {}
documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel') and d.blocking_level != 'error')
for edi_doc in documents:
move = edi_doc.move_id
edi_format = edi_doc.edi_format_id
if move.is_invoice(include_receipts=True):
doc_type = 'invoice'
elif move.payment_id or move.statement_line_id:
doc_type = 'payment'
else:
continue

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
for state, edi_flow in (('to_send', 'post'), ('to_cancel', 'cancel')):
documents = self.filtered(lambda d: d.state == state and d.blocking_level != 'error')
for edi_doc in documents:
edi_format = edi_doc.edi_format_id
move = edi_doc.move_id
move_applicability = edi_doc.edi_format_id._get_move_applicability(move) or {}

batching_key = [edi_format, state, move.company_id]
custom_batching_key = f'{edi_flow}_batching'
if move_applicability.get(custom_batching_key):
batching_key += list(move_applicability[custom_batching_key](move))
else:
target.append((doc, doc_type))
if batch:
target.append((batch, doc_type))
return invoices + payments
batching_key.append(move.id)

batch = to_process.setdefault(tuple(batching_key), {
'documents': self.env['account.edi.document'],
'method_to_call': move_applicability.get(edi_flow),
})
batch['documents'] |= edi_doc

return list(to_process.values())

@api.model
def _process_job(self, documents, doc_type):
def _process_job(self, job):
"""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 ?
:param job: {
'documents': account.edi.document,
'method_to_call': str,
}
"""
def _postprocess_post_edi_results(documents, edi_result):
attachments_to_unlink = self.env['ir.attachment']
Expand Down Expand Up @@ -182,38 +174,36 @@ def _postprocess_cancel_edi_results(documents, edi_result):
# supposed to have any traceability from the user.
attachments_to_unlink.sudo().unlink()

documents = job['documents']
if job['method_to_call']:
method_to_call = job['method_to_call']
else:
method_to_call = lambda moves: {move: {'success': True} for move in moves}
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')

edi_format = documents.edi_format_id
state = documents[0].state
documents.move_id.line_ids.flush_recordset() # manual flush for tax details
if doc_type == 'invoice':
if state == 'to_send':
invoices = documents.move_id
with invoices._send_only_when_ready():
edi_result = edi_format._post_invoice_edi(invoices)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = edi_format._cancel_invoice_edi(documents.move_id)
_postprocess_cancel_edi_results(documents, edi_result)

elif doc_type == 'payment':
if state == 'to_send':
edi_result = edi_format._post_payment_edi(documents.move_id)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = edi_format._cancel_payment_edi(documents.move_id)
_postprocess_cancel_edi_results(documents, edi_result)
moves = documents.move_id
if state == 'to_send':
if all(move.is_invoice(include_receipts=True) for move in moves):
with moves._send_only_when_ready():
edi_result = method_to_call(moves)
else:
edi_result = method_to_call(moves)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
edi_result = method_to_call(moves)
_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()
for documents, doc_type in jobs:
self._process_job(documents, doc_type)
for job in jobs:
self._process_job(job)

def _process_documents_web_services(self, job_count=None, with_commit=True):
''' Post and cancel all the documents that need a web service.
Expand All @@ -225,7 +215,8 @@ def _process_documents_web_services(self, job_count=None, with_commit=True):
all_jobs = self.filtered(lambda d: d.edi_format_id._needs_web_services())._prepare_jobs()
jobs_to_process = all_jobs[0:job_count] if job_count else all_jobs

for documents, doc_type in jobs_to_process:
for job in jobs_to_process:
documents = job['documents']
move_to_lock = documents.move_id
attachments_potential_unlink = documents.attachment_id.filtered(lambda a: not a.res_model and not a.res_id)
try:
Expand All @@ -245,7 +236,7 @@ def _process_documents_web_services(self, job_count=None, with_commit=True):
continue
else:
raise e
self._process_job(documents, doc_type)
self._process_job(job)
if with_commit and len(jobs_to_process) > 1:
self.env.cr.commit()

Expand Down
118 changes: 9 additions & 109 deletions addons/account_edi/models/account_edi_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class AccountEdiFormat(models.Model):
('unique_code', 'unique (code)', 'This code already exists')
]


####################################################
# Low-level methods
####################################################
Expand All @@ -51,37 +50,18 @@ def create(self, vals_list):
# Export method to override based on EDI Format
####################################################

def _get_invoice_edi_content(self, move):
''' Create a bytes literal of the file content representing the invoice - to be overridden by the EDI Format
:returns: bytes literal of the content generated (typically XML).
'''
return b''

def _get_payment_edi_content(self, move):
''' Create a bytes literal of the file content representing the payment - to be overridden by the EDI Format
:returns: bytes literal of the content generated (typically XML).
'''
return b''

def _is_required_for_invoice(self, invoice):
""" Indicate if this EDI must be generated for the invoice passed as parameter.
def _get_move_applicability(self, move):
""" Core function for the EDI processing: it first checks whether the EDI format is applicable on a given
move, if so, it then returns a dictionary containing the functions to call for this move.
:param invoice: An account.move having the invoice type.
:returns: True if the EDI must be generated, False otherwise.
:return: dict mapping str to function (callable)
* post: function called for edi.documents with state 'to_send' (post flow)
* cancel: function called for edi.documents with state 'to_cancel' (cancel flow)
* post_batching: function returning the batching key for the post flow
* cancel_batching: function returning the batching key for the cancel flow
* edi_content: function called when computing the edi_content for an edi.document
"""
# TO OVERRIDE
self.ensure_one()
return True

def _is_required_for_payment(self, payment):
""" Indicate if this EDI must be generated for the payment passed as parameter.
:param payment: An account.move linked to either an account.payment, either an account.bank.statement.line.
:returns: True if the EDI must be generated, False otherwise.
"""
# TO OVERRIDE
self.ensure_one()
return False

def _needs_web_services(self):
""" Indicate if the EDI must be generated asynchronously through to some web services.
Expand Down Expand Up @@ -111,33 +91,6 @@ def _is_enabled_by_default_on_journal(self, journal):
"""
return True

def _support_batching(self, move, state, company):
""" 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.
:param move: The move that we are trying to batch.
:param state: The EDI state of the move.
:param company: The company with which we are sending the EDI.
:returns: True if batching is supported, False otherwise.
"""
# 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 (). It's not necessary to repeat those
fields in the custom key.
:param move: The move to batch.
:param state: The EDI state of the move.
: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).
Expand All @@ -147,59 +100,6 @@ def _check_move_configuration(self, move):
# TO OVERRIDE
return []

def _post_invoice_edi(self, invoices):
""" Create the file content representing the invoice (and calls web services if necessary).
:param invoices: A list of invoices to post.
:returns: A dictionary with the invoice as key and as value, another dictionary:
* success: True if the edi was successfully posted.
* attachment: The attachment representing the invoice in this edi_format.
* error: An error if the edi was not successfully posted.
* blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
return {}

def _cancel_invoice_edi(self, invoices):
"""Calls the web services to cancel the invoice of this document.
:param invoices: A list of invoices to cancel.
: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) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
return {invoice: {'success': True} for invoice in invoices} # By default, cancel succeeds doing nothing.

def _post_payment_edi(self, payments):
""" Create the file content representing the payment (and calls web services if necessary).
:param payments: The payments to post.
: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) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
return {}

def _cancel_payment_edi(self, payments):
"""Calls the web services to cancel the payment of this document.
:param payments: A list of payments to cancel.
: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) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
return {payment: {'success': True} for payment in payments} # By default, cancel succeeds doing nothing.

####################################################
# Import methods to override based on EDI Format
####################################################
Expand Down
Loading

0 comments on commit 0e5626c

Please sign in to comment.