19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from psycopg2 import OperationalError
import base64
import logging
from collections import defaultdict
from odoo import models, fields, api, _
from odoo.exceptions import LockError, UserError
_logger = logging.getLogger(__name__)
@ -38,15 +35,12 @@ class AccountEdiDocument(models.Model):
# == Not stored fields ==
name = fields.Char(related='attachment_id.name')
edi_format_name = fields.Char(string='Format Name', related='edi_format_id.name')
edi_content = fields.Binary(compute='_compute_edi_content', compute_sudo=True)
edi_content = fields.Binary(compute='_compute_edi_content')
_sql_constraints = [
(
'unique_edi_document_by_move_by_format',
'UNIQUE(edi_format_id, move_id)',
'Only one edi document by move by format',
),
]
_unique_edi_document_by_move_by_format = models.Constraint(
'UNIQUE(edi_format_id, move_id)',
'Only one edi document by move by format',
)
@api.depends('move_id', 'error', 'state')
def _compute_edi_content(self):
@ -73,7 +67,7 @@ class AccountEdiDocument(models.Model):
def _prepare_jobs(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
doc_type invoice and company_id AND the edi_format_id supports batching, they are grouped
into a single job.
:returns: [{
@ -85,9 +79,6 @@ class AccountEdiDocument(models.Model):
to_process = {}
for state, edi_flow in (('to_send', 'post'), ('to_cancel', 'cancel')):
documents = self.filtered(lambda d: d.state == state and d.blocking_level != 'error')
# This is a dict that for each batching key gives the value that should be appended to the batching key,
# in order to enforce a limit of documents per batch.
batching_limit_keys = defaultdict(lambda: 0)
for edi_doc in documents:
edi_format = edi_doc.edi_format_id
move = edi_doc.move_id
@ -100,25 +91,17 @@ class AccountEdiDocument(models.Model):
else:
batching_key.append(move.id)
if move_applicability.get('batching_limit'):
batching_key_without_limit = tuple(batching_key)
batching_key.append(batching_limit_keys[batching_key_without_limit])
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
if move_applicability.get('batching_limit') and len(batch['documents']) >= move_applicability['batching_limit']:
batching_limit_keys[batching_key_without_limit] += 1
return list(to_process.values())
@api.model
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.
"""Post or cancel move_id by calling the related methods on edi_format_id.
:param job: {
'documents': account.edi.document,
@ -141,11 +124,6 @@ class AccountEdiDocument(models.Model):
'error': False,
'blocking_level': False,
})
if move.is_invoice(include_receipts=True):
reconciled_lines = move.line_ids.filtered(lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable'))
reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') \
| reconciled_lines.mapped('matched_credit_ids.credit_move_id')
reconciled_amls.move_id._update_payments_edi_documents()
else:
document.write({
'error': move_result.get('error', False),
@ -212,10 +190,7 @@ class AccountEdiDocument(models.Model):
documents.move_id.line_ids.flush_recordset() # manual flush for tax details
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:
with moves._send_only_when_ready():
edi_result = method_to_call(moves)
_postprocess_post_edi_results(documents, edi_result)
elif state == 'to_cancel':
@ -244,22 +219,14 @@ class AccountEdiDocument(models.Model):
move_to_lock = documents.move_id
attachments_potential_unlink = documents.sudo().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)])
self._cr.execute('SELECT * FROM account_move WHERE id IN %s FOR UPDATE NOWAIT', [tuple(move_to_lock.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)])
except OperationalError as e:
if e.pgcode == '55P03':
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
if not with_commit:
raise UserError(_('This document is being sent by another process already. '))
continue
else:
raise e
documents.lock_for_update()
move_to_lock.lock_for_update()
attachments_potential_unlink.lock_for_update()
except LockError:
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
if not with_commit:
raise UserError(_('This document is being sent by another process already. ')) from None
continue
self._process_job(job)
if with_commit and len(jobs_to_process) > 1:
self.env.cr.commit()
@ -282,3 +249,33 @@ class AccountEdiDocument(models.Model):
# Mark the CRON to be triggered again asap since there is some remaining jobs to process.
if nb_remaining_jobs > 0:
self.env.ref('account_edi.ir_cron_edi_network')._trigger()
def _filter_edi_attachments_for_mailing(self):
"""
Will either return the information about the attachment of the edi document for adding the attachment in the
mail, or the attachment id to be linked to the 'send & print' wizard.
Can be overridden where e.g. a zip-file needs to be sent with the individual files instead of the entire zip
IMPORTANT:
* If the attachment's id is returned, no new attachment will be created, the existing one on the move is linked
to the wizard (see computed attachment_ids field in mail.compose.message).
* If the attachment's content is returned, a new one is created and linked to the wizard. Thus, when sending
the mail (clicking on 'send & print' in the wizard), a new attachment is added to the move (see
_action_send_mail in mail.compose.message).
:param document: an edi document
:return: dict {
'attachments': tuple with the name and base64 content of the attachment}
'attachment_ids': list containing the id of the attachment
}
"""
self.ensure_one()
attachment_sudo = self.sudo().attachment_id
if not attachment_sudo:
return {}
if not (attachment_sudo.res_model and attachment_sudo.res_id):
# do not return system attachment not linked to a record
return {}
if len(self.env.context.get('active_ids', [])) > 1:
# In mass mail mode 'attachments_ids' is removed from template values
# as they should not be rendered
return {'attachments': [(attachment_sudo.name, attachment_sudo.datas)]}
return {'attachment_ids': attachment_sudo.ids}