diff options
Diffstat (limited to 'addons/account_edi/models/account_move.py')
| -rw-r--r-- | addons/account_edi/models/account_move.py | 290 |
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 |
