Files
odoo_source/addons/account_edi/models/account_edi_document.py
T
Jorge Pinna Puissant 7b06e6c8b6 [FIX] account_edi: allow attachment and error in same document
Allow having in an attachment as well as an error in the same document.
This will allow to see the document that generates the error, and easy
the process of debugging.

closes odoo/odoo#60599

X-original-commit: 7313e9823fffcac747af633b9b1694aa661dfa1f
Related: odoo/enterprise#14357
Signed-off-by: Jorge Pinna Puissant (jpp) <jpp@odoo.com>
2020-10-23 09:09:21 +00:00

197 lines
9.2 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from psycopg2 import OperationalError
import logging
_logger = logging.getLogger(__name__)
class AccountEdiDocument(models.Model):
_name = 'account.edi.document'
_description = 'Electronic Document for an account.move'
# == Stored fields ==
move_id = fields.Many2one('account.move', required=True, ondelete='cascade')
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()
# == Not stored fields ==
name = fields.Char(related='attachment_id.name')
edi_format_name = fields.Char(string='Format Name', related='edi_format_id.name')
_sql_constraints = [
(
'unique_edi_document_by_move_by_format',
'UNIQUE(edi_format_id, move_id)',
'Only one edi document by move by format',
),
]
def _prepare_jobs(self):
"""Creates a list of jobs to be performed by '_process_jobs' 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
"""
to_process = []
batches = {}
for edi_doc in self.filtered(lambda d: d.state in ('to_send', 'to_cancel')):
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
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
@api.model
def _process_jobs(self, to_process):
"""Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
Invoices are processed before payments.
"""
def _postprocess_post_edi_results(documents, edi_result):
attachments_to_unlink = self.env['ir.attachment']
for document in documents:
move = document.move_id
move_result = edi_result.get(move, {})
if move_result.get('attachment'):
old_attachment = document.attachment_id
values = {
'attachment_id': move_result['attachment'].id,
'error': move_result.get('error', False),
}
if not values.get('error'):
values.update({'state': 'sent'})
document.write(values)
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."))
# 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.
attachments_to_unlink.unlink()
def _postprocess_cancel_edi_results(documents, edi_result):
invoice_ids_to_cancel = set() # Avoid duplicates
attachments_to_unlink = self.env['ir.attachment']
for document in documents:
move = document.move_id
move_result = edi_result.get(move, {})
if move_result.get('success'):
old_attachment = document.attachment_id
document.write({
'state': 'cancelled',
'error': False,
'attachment_id': False,
})
if move.is_invoice(include_receipts=True) and move.state == 'posted':
# The user requested a cancellation of the EDI and it has been approved. Then, the invoice
# can be safely cancelled.
invoice_ids_to_cancel.add(move.id)
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.")
if invoice_ids_to_cancel:
invoices = self.env['account.move'].browse(list(invoice_ids_to_cancel))
invoices.button_draft()
invoices.button_cancel()
# 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.
attachments_to_unlink.unlink()
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
for documents in batches:
try:
with self.env.cr.savepoint():
# 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(self.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():
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
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)
def _process_documents_web_services(self, job_count=None):
""" 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)])