Files
odoo_source/addons/account_edi/models/account_move.py
T
Benjamin Frantzen (bfr) ce674f1b21 [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.
2021-02-01 14:41:58 +00:00

314 lines
14 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
edi_document_ids = fields.One2many(
comodel_name='account.edi.document',
inverse_name='move_id')
edi_state = fields.Selection(
selection=[('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')],
string="Electronic invoicing",
store=True,
compute='_compute_edi_state',
help='The aggregated state of all the EDIs with web-service of this move')
edi_error_count = fields.Integer(
compute='_compute_edi_error_count',
help='How many EDIs are in error for this move ?')
edi_web_services_to_process = fields.Text(
compute='_compute_edi_web_services_to_process',
help="Technical field to display the documents that will be processed by the CRON")
edi_show_cancel_button = fields.Boolean(
compute='_compute_edi_show_cancel_button')
@api.depends('edi_document_ids.state')
def _compute_edi_state(self):
for move in self:
all_states = set(move.edi_document_ids.filtered(lambda d: d.edi_format_id._needs_web_services()).mapped('state'))
if all_states == {'sent'}:
move.edi_state = 'sent'
elif all_states == {'cancelled'}:
move.edi_state = 'cancelled'
elif 'to_send' in all_states:
move.edi_state = 'to_send'
elif 'to_cancel' in all_states:
move.edi_state = 'to_cancel'
else:
move.edi_state = False
@api.depends('edi_document_ids.error')
def _compute_edi_error_count(self):
for move in self:
move.edi_error_count = len(move.edi_document_ids.filtered(lambda d: d.error))
@api.depends(
'edi_document_ids',
'edi_document_ids.state',
'edi_document_ids.edi_format_id',
'edi_document_ids.edi_format_id.name')
def _compute_edi_web_services_to_process(self):
for move in self:
to_process = move.edi_document_ids.filtered(lambda d: d.state in ['to_send', 'to_cancel'])
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('restrict_mode_hash_table', 'state')
def _compute_show_reset_to_draft_button(self):
# OVERRIDE
super()._compute_show_reset_to_draft_button()
for move in self:
for doc in move.edi_document_ids:
if doc.edi_format_id._needs_web_services() \
and doc.attachment_id \
and doc.state in ('sent', 'to_cancel') \
and move.is_invoice(include_receipts=True) \
and doc.edi_format_id._is_required_for_invoice(move):
move.show_reset_to_draft_button = False
break
@api.depends(
'state',
'edi_document_ids.state',
'edi_document_ids.attachment_id')
def _compute_edi_show_cancel_button(self):
for move in self:
if move.state != 'posted':
move.edi_show_cancel_button = False
continue
move.edi_show_cancel_button = any([doc.edi_format_id._needs_web_services()
and doc.attachment_id
and doc.state == 'sent'
and move.is_invoice(include_receipts=True)
and doc.edi_format_id._is_required_for_invoice(move)
for doc in move.edi_document_ids])
####################################################
# Export Electronic Document
####################################################
def _update_payments_edi_documents(self):
''' Update the edi documents linked to the current journal entries. These journal entries must be linked to an
account.payment of an account.bank.statement.line. This additional method is needed because the payment flow is
not the same as the invoice one. Indeed, the edi documents must be updated when the reconciliation with some
invoices is changing.
'''
edi_document_vals_list = []
for payment in self:
edi_formats = payment._get_reconciled_invoices().journal_id.edi_format_ids + payment.edi_document_ids.edi_format_id
edi_formats = self.env['account.edi.format'].browse(edi_formats.ids) # Avoid duplicates
for edi_format in edi_formats:
existing_edi_document = payment.edi_document_ids.filtered(lambda x: x.edi_format_id == edi_format)
if edi_format._is_required_for_payment(payment):
if existing_edi_document:
existing_edi_document.write({
'state': 'to_send',
'error': False,
'blocking_level': False,
})
else:
edi_document_vals_list.append({
'edi_format_id': edi_format.id,
'move_id': payment.id,
'state': 'to_send',
})
elif existing_edi_document:
existing_edi_document.write({
'state': False,
'error': False,
'blocking_level': False,
})
self.env['account.edi.document'].create(edi_document_vals_list)
self.edi_document_ids._process_documents_no_web_services()
def _post(self, soft=True):
# OVERRIDE
# Set the electronic document to be posted and post immediately for synchronous formats.
posted = super()._post(soft=soft)
edi_document_vals_list = []
for move in posted:
for edi_format in move.journal_id.edi_format_ids:
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({
'state': 'to_send',
'attachment_id': False,
})
else:
edi_document_vals_list.append({
'edi_format_id': edi_format.id,
'move_id': move.id,
'state': 'to_send',
})
self.env['account.edi.document'].create(edi_document_vals_list)
posted.edi_document_ids._process_documents_no_web_services()
return posted
def button_cancel(self):
# OVERRIDE
# 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, '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
def button_draft(self):
# OVERRIDE
for move in self:
if move.edi_show_cancel_button:
raise UserError(_(
"You can't edit the following journal entry %s because an electronic document has already been "
"sent. Please use the 'Request EDI Cancellation' button instead."
) % move.display_name)
res = super().button_draft()
self.edi_document_ids.write({'state': False, 'error': False, 'blocking_level': False})
return res
def button_cancel_posted_moves(self):
'''Mark the edi.document related to this move to be canceled.
'''
to_cancel_documents = self.env['account.edi.document']
for move in self:
is_move_marked = False
for doc in move.edi_document_ids:
if doc.edi_format_id._needs_web_services() \
and doc.attachment_id \
and doc.state == 'sent' \
and move.is_invoice(include_receipts=True) \
and doc.edi_format_id._is_required_for_invoice(move):
to_cancel_documents |= doc
is_move_marked = True
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, '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
####################################################
@api.returns('mail.message', lambda value: value.id)
def message_post(self, **kwargs):
# OVERRIDE
# When posting a message, analyse the attachment to check if it is an EDI document and update the invoice
# with the imported data.
res = super().message_post(**kwargs)
if len(self) != 1 or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True):
return res
attachments = self.env['ir.attachment'].browse(kwargs.get('attachment_ids', []))
odoobot = self.env.ref('base.partner_root')
if attachments and self.state != 'draft':
self.message_post(body='The invoice is not a draft, it was not updated from the attachment.',
message_type='comment',
subtype_xmlid='mail.mt_note',
author_id=odoobot.id)
return res
if attachments and self.line_ids:
self.message_post(body='The invoice already contains lines, it was not updated from the attachment.',
message_type='comment',
subtype_xmlid='mail.mt_note',
author_id=odoobot.id)
return res
edi_formats = self.env['account.edi.format'].search([])
for attachment in attachments:
invoice = edi_formats._update_invoice_from_attachment(attachment, self)
if invoice:
break
return res
####################################################
# Business operations
####################################################
def action_process_edi_web_services(self):
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'
####################################################
# Export Electronic Document
####################################################
def reconcile(self):
# OVERRIDE
# In some countries, the payments must be sent to the government under some condition. One of them could be
# there is at least one reconciled invoice to the payment. Then, we need to update the state of the edi
# documents during the reconciliation.
all_lines = self + self.matched_debit_ids.debit_move_id + self.matched_credit_ids.credit_move_id
payments = all_lines.move_id.filtered(lambda move: move.payment_id or move.statement_line_id)
invoices_per_payment_before = {pay: pay._get_reconciled_invoices() for pay in payments}
res = super().reconcile()
invoices_per_payment_after = {pay: pay._get_reconciled_invoices() for pay in payments}
changed_payments = self.env['account.move']
for payment, invoices_after in invoices_per_payment_after.items():
invoices_before = invoices_per_payment_before[payment]
if set(invoices_after.ids) != set(invoices_before.ids):
changed_payments |= payment
changed_payments._update_payments_edi_documents()
return res
def remove_move_reconcile(self):
# OVERRIDE
# When a payment has been sent to the government, it usually contains some information about reconciled
# invoices. If the user breaks a reconciliation, the related payments must be cancelled properly and then, a new
# electronic document must be generated.
all_lines = self + self.matched_debit_ids.debit_move_id + self.matched_credit_ids.credit_move_id
payments = all_lines.move_id.filtered(lambda move: move.payment_id or move.statement_line_id)
invoices_per_payment_before = {pay: pay._get_reconciled_invoices() for pay in payments}
res = super().remove_move_reconcile()
invoices_per_payment_after = {pay: pay._get_reconciled_invoices() for pay in payments}
changed_payments = self.env['account.move']
for payment, invoices_after in invoices_per_payment_after.items():
invoices_before = invoices_per_payment_before[payment]
if set(invoices_after.ids) != set(invoices_before.ids):
changed_payments |= payment
changed_payments._update_payments_edi_documents()
return res