summaryrefslogtreecommitdiff
path: root/addons/account_edi/models/account_move.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/account_edi/models/account_move.py')
-rw-r--r--addons/account_edi/models/account_move.py290
1 files changed, 290 insertions, 0 deletions
diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py
new file mode 100644
index 00000000..3faa9e14
--- /dev/null
+++ b/addons/account_edi/models/account_move.py
@@ -0,0 +1,290 @@
+# -*- 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 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
+ ####################################################
+
+ def _get_create_invoice_from_attachment_decoders(self):
+ # OVERRIDE
+ res = super()._get_create_invoice_from_attachment_decoders()
+ res.append((10, self.env['account.edi.format'].search([])._create_invoice_from_attachment))
+ return res
+
+ def _get_update_invoice_from_attachment_decoders(self, invoice):
+ # OVERRIDE
+ res = super()._get_update_invoice_from_attachment_decoders(invoice)
+ res.append((10, self.env['account.edi.format'].search([])._update_invoice_from_attachment))
+ 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')
+ 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