- 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.
314 lines
14 KiB
Python
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
|