# -*- coding: utf-8 -*- from .common import TestMxEdiCommon, EXTERNAL_MODE from odoo import fields, Command from odoo.exceptions import UserError from odoo.tests import tagged from odoo.tools import misc from odoo.tools.misc import file_open from datetime import datetime from dateutil.relativedelta import relativedelta from lxml import etree from pytz import timezone from odoo.addons.l10n_mx_edi.models.l10n_mx_edi_document import CFDI_DATE_FORMAT @tagged('post_install_l10n', 'post_install', '-at_install', *(['-standard', 'external'] if EXTERNAL_MODE else [])) class TestCFDIInvoice(TestMxEdiCommon): def test_invoice_misc_business_values(self): for move_type, output_file in ( ('out_invoice', 'test_misc_business_values_invoice'), ('out_refund', 'test_misc_business_values_credit_note') ): with self.mx_external_setup(self.frozen_today), self.subTest(move_type=move_type): invoice = self._create_invoice( invoice_incoterm_id=self.env.ref('account.incoterm_FCA').id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 2000.0, 'quantity': 5, 'discount': 20.0, }), # Ignored lines by the CFDI: Command.create({ 'product_id': self.product.id, 'price_unit': 2000.0, 'quantity': 0.0, }), Command.create({ 'product_id': self.product.id, 'price_unit': 0.0, 'quantity': 10.0, }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, output_file) def test_customer_in_mx(self): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice() with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_customer_in_mx_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, 'test_customer_in_mx_pay') def test_customer_in_us(self): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice(partner_id=self.partner_us.id) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_customer_in_us_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, 'test_customer_in_us_pay') def test_customer_no_country(self): with self.mx_external_setup(self.frozen_today): self.partner_us.country_id = None invoice = self._create_invoice(partner_id=self.partner_us.id) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_customer_no_country_inv') def test_customer_in_mx_to_public(self): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice(l10n_mx_edi_cfdi_to_public=True) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_customer_in_mx_to_public_inv') def test_invoice_taxes(self): def create_invoice(taxes_list, l10n_mx_edi_cfdi_to_public=False): invoice_line_ids = [] for i, taxes in enumerate(taxes_list, start=1): invoice_line_ids.append(Command.create({ 'product_id': self.product.id, 'price_unit': 1000.0 * i, 'quantity': 5, 'discount': 20.0, 'tax_ids': [Command.set(taxes.ids)], })) # Full discounted line: invoice_line_ids.append(Command.create({ 'product_id': self.product.id, 'price_unit': 1000.0 * i, 'discount': 100.0, 'tax_ids': [Command.set(taxes.ids)], })) return self._create_invoice( invoice_line_ids=invoice_line_ids, l10n_mx_edi_cfdi_to_public=l10n_mx_edi_cfdi_to_public, ) with self.mx_external_setup(self.frozen_today): for index, taxes_list in enumerate(self.existing_taxes_combinations_to_test, start=1): with self.subTest(index=index): # Test the invoice CFDI. self.partner_mx.l10n_mx_edi_no_tax_breakdown = False invoice = create_invoice(taxes_list) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_invoice_taxes_{index}_invoice') # Test the payment CFDI. payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_invoice_taxes_{index}_payment') # Test the global invoice CFDI. invoice = create_invoice(taxes_list, l10n_mx_edi_cfdi_to_public=True) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_global_invoice_try_send() self._assert_global_invoice_cfdi_from_invoices(invoice, f'test_invoice_taxes_{index}_ginvoice') # Test the invoice with no tax breakdown. self.partner_mx.l10n_mx_edi_no_tax_breakdown = True invoice = create_invoice(taxes_list) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_invoice_taxes_{index}_invoice_no_tax_breakdown') def test_invoice_addenda(self): with self.mx_external_setup(self.frozen_today): self.partner_mx.l10n_mx_edi_addenda = self.env['ir.ui.view'].create({ 'name': 'test_invoice_cfdi_addenda', 'type': 'qweb', 'arch': """ """ }) invoice = self._create_invoice() with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_invoice_addenda') def test_invoice_negative_lines_dispatch_same_product(self): """ Ensure the distribution of negative lines is done on the same product first. """ product1 = self.product product2 = self._create_product() with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': product1.id, 'quantity': 5.0, 'tax_ids': [], }), Command.create({ 'product_id': product2.id, 'quantity': -5.0, 'tax_ids': [], }), Command.create({ 'product_id': product2.id, 'quantity': 12.0, 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_invoice_negative_lines_dispatch_same_product') def test_invoice_negative_lines_dispatch_same_amount(self): """ Ensure the distribution of negative lines is done on the same amount. """ with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 12.0, 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': 3.0, 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': 6.0, 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': -3.0, 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_invoice_negative_lines_dispatch_same_amount') def test_invoice_negative_lines_dispatch_same_taxes(self): """ Ensure the distribution of negative lines is done exclusively on lines having the same taxes. """ product1 = self.product product2 = self._create_product() with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': product1.id, 'quantity': 12.0, 'tax_ids': [], }), Command.create({ 'product_id': product1.id, 'quantity': 3.0, 'tax_ids': [], }), Command.create({ 'product_id': product2.id, 'quantity': 6.0, 'tax_ids': [Command.set(self.tax_16.ids)], }), Command.create({ 'product_id': product1.id, 'quantity': -3.0, 'tax_ids': [Command.set(self.tax_16.ids)], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_invoice_negative_lines_dispatch_same_taxes') def test_invoice_negative_lines_dispatch_biggest_amount(self): """ Ensure the distribution of negative lines is done on the biggest amount. """ with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 3.0, 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': 12.0, 'discount': 10.0, # price_subtotal: 10800 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': 8.0, 'discount': 20.0, # price_subtotal: 6400 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': -22.0, 'discount': 22.0, # price_subtotal: 17160 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_invoice_negative_lines_dispatch_biggest_amount') def test_invoice_negative_lines_zero_total(self): """ Test an invoice completely refunded by the negative lines. """ with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 12.0, 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': -12.0, 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'move_id': invoice.id, 'state': 'invoice_sent', 'attachment_id': False, 'cancel_button_needed': False, }]) def test_invoice_negative_lines_orphan_negative_line(self): """ Test an invoice in which a negative line failed to be distributed. """ with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 12.0, 'tax_ids': [Command.set(self.tax_16.ids)], }), Command.create({ 'product_id': self.product.id, 'quantity': -2.0, 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'move_id': invoice.id, 'state': 'invoice_sent_failed', }]) def test_global_invoice_negative_lines_zero_total(self): """ Test an invoice completely refunded by the negative lines. """ with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 12.0, 'tax_ids': [], }), Command.create({ 'product_id': self.product.id, 'quantity': -12.0, 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): self.env['l10n_mx_edi.global_invoice.create'] \ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context']) \ .create({})\ .action_create_global_invoice() self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'invoice_ids': invoice.ids, 'state': 'ginvoice_sent', 'attachment_id': False, 'cancel_button_needed': False, }]) def test_global_invoice_negative_lines_orphan_negative_line(self): """ Test a global invoice containing an invoice having a negative line that failed to be distributed. """ with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 12.0, 'tax_ids': [Command.set(self.tax_16.ids)], }), Command.create({ 'product_id': self.product.id, 'quantity': -2.0, 'tax_ids': [], }), ], ) with self.with_mocked_pac_sign_success(): self.env['l10n_mx_edi.global_invoice.create'] \ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context']) \ .create({})\ .action_create_global_invoice() self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'invoice_ids': invoice.ids, 'state': 'ginvoice_sent_failed', }]) def test_global_invoice_including_partial_refund(self): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 10.0, }), Command.create({ 'product_id': self.product.id, 'quantity': -2.0, }), ], ) refund = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, move_type='out_refund', invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 3.0, }), Command.create({ 'product_id': self.product.id, 'quantity': -1.0, }), ], reversed_entry_id=invoice.id, ) invoices = invoice + refund with self.with_mocked_pac_sign_success(): # Calling the global invoice on the invoice will include the refund automatically. self.env['l10n_mx_edi.global_invoice.create']\ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({})\ .action_create_global_invoice() self._assert_global_invoice_cfdi_from_invoices(invoices, 'test_global_invoice_including_partial_refund') self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'invoice_ids': invoices.ids, 'state': 'ginvoice_sent', }]) def test_global_invoice_including_full_refund(self): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 10.0, }), ], ) refund = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, move_type='out_refund', invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 10.0, }), ], reversed_entry_id=invoice.id, ) invoices = invoice + refund with self.with_mocked_pac_sign_success(): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(invoices.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({})\ .action_create_global_invoice() self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'invoice_ids': invoices.ids, 'state': 'ginvoice_sent', 'attachment_id': False, }]) def test_global_invoice_not_allowed_refund(self): with self.mx_external_setup(self.frozen_today): refund = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, move_type='out_refund', invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 3.0, }), ], ) with self.assertRaises(UserError): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(refund.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({})\ .action_create_global_invoice() def test_global_invoice_refund_after(self): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 10.0, }), ], ) with self.with_mocked_pac_sign_success(): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({})\ .action_create_global_invoice() self.assertRecordValues(invoice.l10n_mx_edi_invoice_document_ids, [{ 'invoice_ids': invoice.ids, 'state': 'ginvoice_sent', }]) refund = self._create_invoice( l10n_mx_edi_cfdi_to_public=True, move_type='out_refund', invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': 3.0, }), ], reversed_entry_id=invoice.id, ) with self.assertRaises(UserError): self.env['l10n_mx_edi.global_invoice.create'] \ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context']) \ .create({}) with self.with_mocked_pac_sign_success(): self.env['account.move.send']\ .with_context(active_model=refund._name, active_ids=refund.ids)\ .create({})\ .action_send_and_print() self._assert_invoice_cfdi(refund, 'test_global_invoice_refund_after') self.assertRecordValues(refund.l10n_mx_edi_invoice_document_ids, [{ 'move_id': refund.id, 'invoice_ids': refund.ids, 'state': 'invoice_sent', }]) def test_invoice_company_branch(self): self.env.company.write({ 'child_ids': [Command.create({ 'name': 'Branch A', 'zip': '85120', 'l10n_mx_edi_certificate_ids': [Command.set(self.certificate.ids)], })], }) self.cr.precommit.run() # load the CoA branch = self.env.company.child_ids self.product.company_id = branch with self.mx_external_setup(self.frozen_today - relativedelta(hours=1)): invoice = self._create_invoice(company_id=branch.id) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, 'test_invoice_company_branch') def test_invoice_then_refund(self): # Create an invoice then sign it. with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice(l10n_mx_edi_cfdi_to_public=True) with self.with_mocked_pac_sign_success(): self.env['account.move.send']\ .with_context(active_model=invoice._name, active_ids=invoice.ids)\ .create({})\ .action_send_and_print() self._assert_invoice_cfdi(invoice, 'test_invoice_then_refund_1') # You are no longer able to create a global invoice. with self.assertRaises(UserError): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({}) # Create a refund. results = self.env['account.move.reversal']\ .with_context(active_model='account.move', active_ids=invoice.ids)\ .create({ 'reason': "turlututu", 'journal_id': invoice.journal_id.id, })\ .refund_moves() refund = self.env['account.move'].browse(results['res_id']) refund.auto_post = 'no' refund.action_post() # You can't make a global invoice for it. with self.assertRaises(UserError): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(refund.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({}) # Create the CFDI and sign it. with self.with_mocked_pac_sign_success(): self.env['account.move.send']\ .with_context(active_model=refund._name, active_ids=refund.ids)\ .create({})\ .action_send_and_print() self._assert_invoice_cfdi(refund, 'test_invoice_then_refund_2') self.assertRecordValues(refund, [{ 'l10n_mx_edi_cfdi_origin': f'01|{invoice.l10n_mx_edi_cfdi_uuid}', }]) def test_global_invoice_then_refund(self): # Create a global invoice and sign it. with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice(l10n_mx_edi_cfdi_to_public=True) with self.with_mocked_pac_sign_success(): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(invoice.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({})\ .action_create_global_invoice() self._assert_global_invoice_cfdi_from_invoices(invoice, 'test_global_invoice_then_refund_1') # You are not able to create an invoice for it. wizard = self.env['account.move.send']\ .with_context(active_model=invoice._name, active_ids=invoice.ids)\ .create({}) self.assertRecordValues(wizard, [{'l10n_mx_edi_enable_cfdi': False}]) # Refund the invoice. results = self.env['account.move.reversal']\ .with_context(active_model='account.move', active_ids=invoice.ids)\ .create({ 'reason': "turlututu", 'journal_id': invoice.journal_id.id, })\ .refund_moves() refund = self.env['account.move'].browse(results['res_id']) refund.auto_post = 'no' refund.action_post() # You can't do a global invoice for a refund with self.assertRaises(UserError): self.env['l10n_mx_edi.global_invoice.create']\ .with_context(refund.l10n_mx_edi_action_create_global_invoice()['context'])\ .create({}) # Sign the refund. with self.with_mocked_pac_sign_success(): self.env['account.move.send']\ .with_context(active_model=refund._name, active_ids=refund.ids)\ .create({})\ .action_send_and_print() self._assert_invoice_cfdi(refund, 'test_global_invoice_then_refund_2') def test_invoice_send_and_print_fallback_pdf(self): # Trigger an error when generating the CFDI self.product.unspsc_code_id = False with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 1000.0, 'tax_ids': [Command.set(self.tax_0.ids)], }), ], ) template = self.env.ref(invoice._get_mail_template()) invoice.with_context(skip_invoice_sync=True)._generate_pdf_and_send_invoice(template, force_synchronous=True, allow_fallback_pdf=True) self.assertFalse(invoice.invoice_pdf_report_id, "invoice_pdf_report_id shouldn't be set with the proforma PDF.") def test_import_invoice(self): file_name = "test_import_bill" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') self.partner_mx.company_id = self.env.company # Read the problematic xml file that kept causing crash on bill uploads with file_open(full_file_path, "rb") as file: new_invoice = self._upload_document_on_journal( journal=self.company_data['default_journal_sale'], content=file.read(), filename=file_name, ) self.assertRecordValues(new_invoice, [{ 'currency_id': self.comp_curr.id, 'partner_id': self.partner_mx.id, 'amount_tax': 80.0, 'amount_untaxed': 500.0, 'amount_total': 580.0, 'invoice_date': fields.Date.from_string('2024-04-08'), 'l10n_mx_edi_payment_method_id': self.env.ref('l10n_mx_edi.payment_method_efectivo').id, 'l10n_mx_edi_usage': 'G03', 'l10n_mx_edi_cfdi_uuid': '8CA06290-4800-4F93-8B1B-25B208BB1AFF', }]) self.assertRecordValues(new_invoice.invoice_line_ids, [{ 'quantity': 1.0, 'price_unit': 500.0, 'discount': 0.0, 'product_id': self.product.id, 'product_uom_id': self.product.uom_po_id.id, 'tax_ids': self.tax_16.ids, }]) # The state of the document should be "Sent". self.assertEqual(new_invoice.l10n_mx_edi_invoice_document_ids.state, 'invoice_sent') # The "Update SAT" button should appear continuously (after posting). new_invoice.action_post() self.assertRecordValues(new_invoice, [{ 'need_cancel_request': True, 'l10n_mx_edi_update_sat_needed': True, }]) def test_import_bill(self): # Invoice with payment policy = PUE, otherwise 'FormaPago' (payment method) is set to '99' ('Por Definir') # and the initial payment method cannot be backtracked at import file_name = "test_import_bill" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') # company's partner is not linked by default to its company. self.env.company.partner_id.company_id = self.env.company # Read the problematic xml file that kept causing crash on bill uploads with file_open(full_file_path, "rb") as file: file_content = file.read() new_bill = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file_content, filename=file_name, ) self.assertRecordValues(new_bill, [{ 'currency_id': self.comp_curr.id, 'partner_id': self.env.company.partner_id.id, 'amount_tax': 80.0, 'amount_untaxed': 500.0, 'amount_total': 580.0, 'invoice_date': fields.Date.from_string('2024-04-08'), 'l10n_mx_edi_payment_method_id': self.env.ref('l10n_mx_edi.payment_method_efectivo').id, 'l10n_mx_edi_usage': 'G03', 'l10n_mx_edi_cfdi_uuid': '8CA06290-4800-4F93-8B1B-25B208BB1AFF', }]) self.assertRecordValues(new_bill.invoice_line_ids, [{ 'quantity': 1.0, 'price_unit': 500.0, 'discount': 0.0, 'product_id': self.product.id, 'product_uom_id': self.product.uom_po_id.id, 'tax_ids': self.tax_16_purchase.ids, }]) # The state of the document should be "Sent". self.assertEqual(new_bill.l10n_mx_edi_invoice_document_ids.state, 'invoice_received') # The "Update SAT" button should appear continuously (after posting). new_bill.action_post() self.assertTrue(new_bill.l10n_mx_edi_update_sat_needed) with self.with_mocked_sat_call(lambda _x: 'valid'): new_bill.l10n_mx_edi_cfdi_try_sat() self.assertTrue(new_bill.l10n_mx_edi_update_sat_needed) # Check the error about duplicated fiscal folio. new_bill_same_folio = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file_content, filename=file_name, ) new_bill_same_folio.action_post() self.assertRecordValues(new_bill_same_folio, [{'duplicated_ref_ids': new_bill.ids}]) def test_add_cfdi_on_existing_bill_without_cfdi(self): file_name = 'test_add_cfdi_on_existing_bill' file_path = f'{self.test_module}/tests/test_files/{file_name}.xml' with file_open(file_path, 'rb') as file: cfdi_invoice = file.read() attachment = self.env['ir.attachment'].create({ 'mimetype': 'application/xml', 'name': f'{file_name}.xml', 'raw': cfdi_invoice, }) bill = self.env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': self.partner_mx.id, 'date': self.frozen_today.date(), 'invoice_date': self.frozen_today.date(), 'invoice_line_ids': [Command.create({'product_id': self.product.id})], }) prev_invoice_line_ids = bill.invoice_line_ids # Bill was created without a cfdi invoice self.assertRecordValues(bill, [{ 'l10n_mx_edi_cfdi_attachment_id': None, 'l10n_mx_edi_cfdi_uuid': None, }]) # post message with the cfdi invoice attached bill.message_post(attachment_ids=attachment.ids) # check that the uuid is now set and the cfdi attachment is linked but the invoice lines did not change self.assertRecordValues(bill, [{ 'l10n_mx_edi_cfdi_attachment_id': attachment.id, 'l10n_mx_edi_cfdi_uuid': '42000000-0000-0000-0000-000000000001', 'invoice_line_ids': prev_invoice_line_ids.ids, }]) def test_add_cfdi_on_existing_bill_with_cfdi(self): # Check that uploading a CFDI on a bill with an existing CFDI doesn't change the fiscal # folio or CFDI document file_name = "test_import_bill" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') self.env.company.partner_id.company_id = self.env.company with file_open(full_file_path, "rb") as file: file_content = file.read() bill = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file_content, filename=file_name, ) file_name = 'test_add_cfdi_on_existing_bill' file_path = f'{self.test_module}/tests/test_files/{file_name}.xml' with file_open(file_path, 'rb') as file: cfdi_invoice = file.read() attachment = self.env['ir.attachment'].create({ 'mimetype': 'application/xml', 'name': f'{file_name}.xml', 'raw': cfdi_invoice, }) initial_uuid = bill.l10n_mx_edi_cfdi_uuid initial_attachment_id = bill.l10n_mx_edi_document_ids.attachment_id.id # post message with a different cfdi invoice attached bill.message_post(attachment_ids=attachment.ids) # check that the uuid and attachment have not changed to those of the attachment self.assertRecordValues(bill, [{ 'l10n_mx_edi_cfdi_uuid': initial_uuid, 'l10n_mx_edi_cfdi_attachment_id': initial_attachment_id, }]) def test_import_bill_with_extento(self): file_name = "test_import_bill_with_extento" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') # Read the problematic xml file that kept causing crash on bill uploads with file_open(full_file_path, "rb") as file: new_bill = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file.read(), filename=file_name, ) self.assertRecordValues(new_bill.invoice_line_ids, ( { 'quantity': 1, 'price_unit': 54017.48, 'tax_ids': self.tax_16_purchase.ids, }, { 'quantity': 1, 'price_unit': 17893.00, 'tax_ids': self.tax_0_exento_purchase.ids, } )) def test_import_bill_without_tax(self): file_name = "test_import_bill_without_tax" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') # Read the problematic xml file that kept causing crash on bill uploads with file_open(full_file_path, "rb") as file: new_bill = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file.read(), filename=file_name, ) self.assertRecordValues(new_bill.invoice_line_ids, ( { 'quantity': 1, 'price_unit': 54017.48, 'tax_ids': self.tax_16_purchase.ids, }, { 'quantity': 1, 'price_unit': 17893.00, # This should be empty due to the error causing missing attribute 'TasaOCuota' to result in empty tax_ids 'tax_ids': [], } )) def test_import_bill_with_withholding(self): file_name = "test_import_bill_with_withholding" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') # Read the problematic xml file that kept causing crash on bill uploads with file_open(full_file_path, "rb") as file: new_invoice = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file.read(), filename=file_name, ) self.assertRecordValues(new_invoice.line_ids.sorted(), ( { 'balance': 147.0, 'tax_ids': (self.tax_16_purchase + self.tax_4_purchase_withholding).ids, 'display_type': 'product', 'tax_line_id': False, }, { 'balance': -5.88, 'tax_ids': [], 'display_type': 'tax', 'tax_line_id': self.tax_4_purchase_withholding.id, }, { 'balance': 23.52, 'tax_ids': [], 'display_type': 'tax', 'tax_line_id': self.tax_16_purchase.id, }, { 'balance': -164.64, 'tax_ids': [], 'display_type': 'payment_term', 'tax_line_id': False, }, )) def test_import_invoice_cfdi_unknown_partner(self): '''Test the import of invoices with unknown partners: * The partner should be created correctly * On the created move the "CFDI to Public" field (l10n_mx_edi_cfdi_to_public) should be set correctly. ''' mx = self.env.ref('base.mx') subtests = [ { 'xml_file': 'test_import_invoice_cfdi_unknown_partner_1', 'expected_invoice_vals': { 'l10n_mx_edi_cfdi_to_public': False, }, 'expected_partner_vals': { 'name': "INMOBILIARIA CVA", 'vat': 'ICV060329BY0', 'country_id': mx.id, 'property_account_position_id': False, 'zip': '26670', }, }, { 'xml_file': 'test_import_invoice_cfdi_unknown_partner_2', 'expected_invoice_vals': { 'l10n_mx_edi_cfdi_to_public': False, }, 'expected_partner_vals': { 'name': "PARTNER_US", 'vat': False, 'country_id': False, 'property_account_position_id': self.env['account.chart.template'].ref('account_fiscal_position_foreign').id, 'zip': False, }, }, { 'xml_file': 'test_import_invoice_cfdi_unknown_partner_3', 'expected_invoice_vals': { 'l10n_mx_edi_cfdi_to_public': True, }, 'expected_partner_vals': { 'name': "INMOBILIARIA CVA", 'vat': False, 'country_id': mx.id, 'property_account_position_id': False, 'zip': False, }, }, ] for subtest in subtests: xml_file = subtest['xml_file'] with self.subTest(msg=xml_file), self.mocked_retrieve_partner(): full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{xml_file}.xml') with file_open(full_file_path, "rb") as file: invoice = self._upload_document_on_journal( journal=self.company_data['default_journal_sale'], content=file.read(), filename=f'{xml_file}.xml', ) self.assertRecordValues(invoice, [subtest['expected_invoice_vals']]) # field 'property_account_position_id' is company dependant partner = invoice.partner_id.with_company(company=invoice.company_id) self.assertRecordValues(partner, [subtest['expected_partner_vals']]) def test_upload_xml_to_generate_invoice_with_exento_tax(self): self.env['account.tax'].search([('name', '=', 'Exento')]).unlink() self.env['account.tax.group'].search([('name', '=', 'Exento')]).unlink() file_name = "test_import_bill_with_extento" full_file_path = misc.file_path(f'{self.test_module}/tests/test_files/{file_name}.xml') with file_open(full_file_path, "rb") as file: new_bill = self._upload_document_on_journal( journal=self.company_data['default_journal_purchase'], content=file.read(), filename=file_name, ) self.assertRecordValues(new_bill.invoice_line_ids, ( { 'quantity': 1, 'price_unit': 54017.48, }, { 'quantity': 1, 'price_unit': 17893.00, } )) def test_cfdi_rounding_1(self): def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 398.28, 'tax_ids': [Command.set(self.tax_16.ids)], }), Command.create({ 'product_id': self.product.id, 'price_unit': 108.62, 'tax_ids': [Command.set(self.tax_16.ids)], }), Command.create({ 'product_id': self.product.id, 'price_unit': 362.07, 'tax_ids': [Command.set(self.tax_16.ids)], })] + [ Command.create({ 'product_id': self.product.id, 'price_unit': 31.9, 'tax_ids': [Command.set(self.tax_16.ids)], }), ] * 12, ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_1_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_1_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_2(self): def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'quantity': quantity, 'price_unit': price_unit, 'discount': discount, 'tax_ids': [Command.set(self.tax_16.ids)], }) for quantity, price_unit, discount in ( (30, 84.88, 13.00), (30, 18.00, 13.00), (3, 564.32, 13.00), (33, 7.00, 13.00), (20, 49.88, 13.00), (100, 3.10, 13.00), (2, 300.00, 13.00), (36, 36.43, 13.00), (36, 15.00, 13.00), (2, 61.08, 0), (2, 13.05, 0), ) ]) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_2_{rounding_method}_inv') payment = self._create_payment(invoice, currency_id=self.comp_curr.id) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_2_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_3(self): today = self.frozen_today today_minus_1 = self.frozen_today - relativedelta(days=1) self.setup_rates(self.usd, (today_minus_1, 1 / 17.187), (today, 1 / 17.0357)) def run(rounding_method): with self.mx_external_setup(today_minus_1): invoice = self._create_invoice( currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 7.34, 'quantity': 200, 'tax_ids': [Command.set(self.tax_16.ids)], }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_3_{rounding_method}_inv') with self.mx_external_setup(today): payment = self._create_payment( invoice, payment_date=today, currency_id=self.usd.id, ) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_3_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_4(self): today = self.frozen_today today_minus_1 = self.frozen_today - relativedelta(days=1) self.setup_rates(self.usd, (today_minus_1, 1 / 16.9912), (today, 1 / 17.068)) def run(rounding_method): with self.mx_external_setup(today_minus_1): invoice1 = self._create_invoice( currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 68.0, 'quantity': 68.25, 'tax_ids': [Command.set(self.tax_16.ids)], }), ], ) with self.with_mocked_pac_sign_success(): invoice1._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice1, f'test_cfdi_rounding_4_{rounding_method}_inv_1') with self.mx_external_setup(today): invoice2 = self._create_invoice( currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 68.0, 'quantity': 24.0, 'tax_ids': [Command.set(self.tax_16.ids)], }), ], ) with self.with_mocked_pac_sign_success(): invoice2._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice2, f'test_cfdi_rounding_4_{rounding_method}_inv_2') invoices = invoice1 + invoice2 with self.mx_external_setup(today): payment = self._create_payment( invoices, amount=7276.68, currency_id=self.usd.id, payment_date=today, ) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_4_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_5(self): def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': price_unit, 'quantity': quantity, }) for quantity, price_unit in ( (412.0, 43.65), (412.0, 43.65), (90.0, 50.04), (500.0, 11.77), (500.0, 34.93), (90.0, 50.04), ) ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_5_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_5_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_6(self): def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': price_unit, 'quantity': quantity, 'discount': 30.0, }) for quantity, price_unit in ( (7.0, 724.14), (4.0, 491.38), (2.0, 318.97), (7.0, 224.14), (6.0, 206.90), (6.0, 129.31), (6.0, 189.66), (16.0, 775.86), (2.0, 7724.14), (2.0, 1172.41), ) ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_6_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_6_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_7(self): def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': price_unit, 'quantity': quantity, 'tax_ids': [Command.set(taxes.ids)], }) for quantity, price_unit, taxes in ( (12.0, 457.92, self.tax_26_5_ieps + self.tax_16), (12.0, 278.04, self.tax_26_5_ieps + self.tax_16), (12.0, 539.76, self.tax_26_5_ieps + self.tax_16), (36.0, 900.0, self.tax_16), ) ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_7_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_7_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_8(self): def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': price_unit, 'quantity': quantity, 'tax_ids': [Command.set(taxes.ids)], }) for quantity, price_unit, taxes in ( (1.0, 244.0, self.tax_0_ieps + self.tax_0), (8.0, 244.0, self.tax_0_ieps + self.tax_0), (1.0, 2531.0, self.tax_0), (1.0, 2820.75, self.tax_6_ieps + self.tax_0), (1.0, 2531.0, self.tax_0), (8.0, 468.0, self.tax_0_ieps + self.tax_0), (1.0, 2820.75, self.tax_6_ieps + self.tax_0), (1.0, 210.28, self.tax_7_ieps), (1.0, 2820.75, self.tax_6_ieps + self.tax_0), ) ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_8_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_8_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_9(self): usd_exchange_rates = ( 1 / 17.1325, 1 / 17.1932, 1 / 17.0398, 1 / 17.1023, 1 / 17.1105, 1 / 16.7457, ) def quick_create_invoice(rate, quantity_and_price_unit): # Only one rate is allowed per day and to make the test working in external_mode, we need to create 6 rates in less than # 3 days. So let's create/unlink the rate. rate = self.setup_rates(self.usd, (self.frozen_today, rate)) invoice = self._create_invoice( currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': price_unit, 'quantity': quantity, 'tax_ids': [Command.set(self.tax_16.ids)], }) for quantity, price_unit in quantity_and_price_unit ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() rate.unlink() return invoice def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice1 = quick_create_invoice( usd_exchange_rates[0], [(80.0, 21.9)], ) invoice2 = quick_create_invoice( usd_exchange_rates[1], [(200.0, 13.36)], ) invoice3 = quick_create_invoice( usd_exchange_rates[1], [(1200.0, 0.36), (1000.0, 0.44), (800.0, 0.44), (800.0, 0.23)], ) invoice4 = quick_create_invoice( usd_exchange_rates[2], [(200.0, 21.9)], ) invoice5 = quick_create_invoice( usd_exchange_rates[3], [(1000.0, 0.36), (500.0, 0.44), (500.0, 0.23), (400.0, 0.87), (200.0, 0.44)], ) invoice6 = quick_create_invoice( usd_exchange_rates[4], [(200.0, 14.4)], ) self.setup_rates(self.usd, (self.frozen_today, usd_exchange_rates[5])) payment = self._create_payment( invoice1 + invoice2 + invoice3 + invoice4 + invoice5 + invoice6, currency_id=self.env.company.currency_id.id, ) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_9_{rounding_method}_pay') self._test_cfdi_rounding(run) def test_cfdi_rounding_10(self): def create_invoice(**kwargs): return self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': price_unit, 'tax_ids': [Command.set(taxes.ids)], }) for price_unit, taxes in ( (550.0, self.tax_0), (505.0, self.tax_16), (495.0, self.tax_16), (560.0, self.tax_0), (475.0, self.tax_16), ) ], **kwargs, ) def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = create_invoice() with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_10_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_10_{rounding_method}_pay') invoice = create_invoice(l10n_mx_edi_cfdi_to_public=True) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_global_invoice_try_send() self._assert_global_invoice_cfdi_from_invoices(invoice, f'test_cfdi_rounding_10_{rounding_method}_ginvoice') self.tax_16.price_include = True self._test_cfdi_rounding(run) def test_cfdi_rounding_11(self): def create_invoice(limit=1, **kwargs): return self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 3.47, 'tax_ids': [Command.set(self.tax_16.ids)], }) for iteration in range(limit) ], **kwargs, ) def run(rounding_method): with self.mx_external_setup(self.frozen_today): invoice = create_invoice(2) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() self._assert_invoice_cfdi(invoice, f'test_cfdi_rounding_11_{rounding_method}_inv') payment = self._create_payment(invoice) with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment.move_id, f'test_cfdi_rounding_11_{rounding_method}_pay') invoices = create_invoice(l10n_mx_edi_cfdi_to_public=True) + create_invoice(l10n_mx_edi_cfdi_to_public=True) with self.with_mocked_pac_sign_success(): invoices._l10n_mx_edi_cfdi_global_invoice_try_send() self._assert_global_invoice_cfdi_from_invoices(invoices, f'test_cfdi_rounding_11_{rounding_method}_ginvoice') self._test_cfdi_rounding(run) def test_partial_payment_1(self): date1 = self.frozen_today - relativedelta(days=2) date2 = self.frozen_today - relativedelta(days=1) date3 = self.frozen_today self.setup_rates(self.chf, (date1, 16.0), (date2, 17.0), (date3, 18.0)) with self.mx_external_setup(date1): invoice = self._create_invoice() # 1160 MXN with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() # Pay 10% in MXN. payment1 = self._create_payment( invoice, amount=116.0, currency_id=self.comp_curr.id, payment_date=date1, ) with self.with_mocked_pac_sign_success(): payment1.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment1.move_id, 'test_partial_payment_1_pay1') # Pay 10% in CHF (rate 1:16) payment2 = self._create_payment( invoice, amount=1856.0, currency_id=self.chf.id, payment_date=date1, ) with self.with_mocked_pac_sign_success(): payment2.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment2.move_id, 'test_partial_payment_1_pay2') with self.mx_external_setup(date2): # Pay 10% in CHF (rate 1:17). payment3 = self._create_payment( invoice, amount=1972.0, currency_id=self.chf.id, payment_date=date2, ) with self.with_mocked_pac_sign_success(): payment3.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment3.move_id, 'test_partial_payment_1_pay3') with self.mx_external_setup(date3): # Pay 10% in CHF (rate 1:18). payment4 = self._create_payment( invoice, amount=2088.0, currency_id=self.chf.id, payment_date=date3, ) with self.with_mocked_pac_sign_success(): payment4.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment4.move_id, 'test_partial_payment_1_pay4') def test_partial_payment_2(self): date1 = self.frozen_today - relativedelta(days=2) date2 = self.frozen_today - relativedelta(days=1) date3 = self.frozen_today self.setup_rates(self.chf, (date1, 16.0), (date2, 17.0), (date3, 18.0)) self.setup_rates(self.usd, (date1, 17.0), (date2, 16.5), (date3, 16.0)) with self.mx_external_setup(date1): invoice = self._create_invoice( currency_id=self.chf.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 16000.0, }), ], ) # 18560 CHF = 1160 MXN with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() with self.mx_external_setup(date2): # Pay 10% in MXN (116 MXN = 1972 CHF). payment1 = self._create_payment( invoice, amount=116.0, currency_id=self.comp_curr.id, payment_date=date2, ) with self.with_mocked_pac_sign_success(): payment1.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment1.move_id, 'test_partial_payment_2_pay1') # Pay 10% in USD (rate 1:16.5) payment2 = self._create_payment( invoice, amount=1914.0, currency_id=self.usd.id, payment_date=date2, ) with self.with_mocked_pac_sign_success(): payment2.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment2.move_id, 'test_partial_payment_2_pay2') with self.mx_external_setup(date3): # Pay 10% in MXN (116 MXN = 2088 CHF). payment3 = self._create_payment( invoice, amount=116.0, currency_id=self.comp_curr.id, payment_date=date3, ) with self.with_mocked_pac_sign_success(): payment3.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment3.move_id, 'test_partial_payment_2_pay3') # Pay 10% in USD (rate 1:16) payment4 = self._create_payment( invoice, amount=1856.0, currency_id=self.usd.id, payment_date=date3, ) with self.with_mocked_pac_sign_success(): payment4.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment4.move_id, 'test_partial_payment_2_pay4') def test_partial_payment_3(self): """ Test a reconciliation chain with reconciliation with credit note in between. """ date1 = self.frozen_today - relativedelta(days=2) date2 = self.frozen_today - relativedelta(days=1) date3 = self.frozen_today self.setup_rates(self.usd, (date1, 17.0), (date2, 16.5), (date3, 17.5)) with self.mx_external_setup(date1): # MXN invoice at rate 19720 USD = 1160 MXN (1:17) invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 1000.0, }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() with self.mx_external_setup(date2): # Pay 1914 USD = 116 MXN (1:16.5) payment1 = self._create_payment( invoice, amount=1914.0, currency_id=self.usd.id, payment_date=date2, ) with self.with_mocked_pac_sign_success(): payment1.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment1.move_id, 'test_partial_payment_3_pay1') self.assertRecordValues(invoice, [{'amount_residual': 1044.0}]) # USD Credit note at rate 1914 USD = 116 MXN (1:16.5) refund = self._create_invoice( move_type='out_refund', currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 1650.0, }), ], ) (refund + invoice).line_ids.filtered(lambda line: line.display_type == 'payment_term').reconcile() self.assertRecordValues(invoice + refund, [ # The refund reconciled 116 MXN: # - 112.59 MXN with the invoice. # - 3.41 MXN as an exchange difference (1972 - 1914) / 17 ~= 3.41) {'amount_residual': 931.41}, {'amount_residual': 0.0}, ]) # Pay 1914 USD = 116 MXN (1:16.5) # The credit note should be subtracted from the residual chain. payment2 = self._create_payment( invoice, amount=1914.0, currency_id=self.usd.id, payment_date=date2, ) with self.with_mocked_pac_sign_success(): payment2.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment2.move_id, 'test_partial_payment_3_pay2') self.assertRecordValues(invoice, [{'amount_residual': 815.41}]) with self.mx_external_setup(date3): # Pay 10% in USD at rate 1:17.5 payment3 = self._create_payment( invoice, amount=2030.0, currency_id=self.usd.id, payment_date=date3, ) with self.with_mocked_pac_sign_success(): payment3.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment3.move_id, 'test_partial_payment_3_pay3') self.assertRecordValues(invoice, [{'amount_residual': 699.41}]) # USD Credit note at rate 2030 USD = 116 MXN (1:17.5) refund = self._create_invoice( move_type='out_refund', currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 1750.0, }), ], ) (refund + invoice).line_ids.filtered(lambda line: line.display_type == 'payment_term').reconcile() self.assertRecordValues(invoice + refund, [ # The refund reconciled 116 MXN with the invoice. # An exchange difference of (2030 - 1972) / 17 ~= 3.41 has been created. {'amount_residual': 580.0}, {'amount_residual': 0.0}, ]) # Pay 10% in USD at rate 1:17.5 # The credit note should be subtracted from the residual chain. payment4 = self._create_payment( invoice, amount=2030.0, currency_id=self.usd.id, payment_date=date3, ) with self.with_mocked_pac_sign_success(): payment4.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(payment4.move_id, 'test_partial_payment_3_pay4') self.assertRecordValues(invoice, [{'amount_residual': 464.0}]) def test_foreign_curr_statement_and_invoice_modify_exchange_move(self): """ Test a bank reconciliation multi currency reconcliation with remaining amount in company currency """ date1 = self.frozen_today - relativedelta(days=1) date2 = self.frozen_today # Rates for how much USD for 1 MXN self.setup_rates(self.usd, (date1, 1.5), (date2, 2.0001)) with self.mx_external_setup(date1): # 2 MXN invoices at rate 100 USD + 16% = 116 USD = 77.3 MXN (1:1.5) invoice1 = self._create_invoice( currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 100, }), ], ) invoice2 = self._create_invoice( currency_id=self.usd.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 100, }), ], ) with self.with_mocked_pac_sign_success(): invoice1._l10n_mx_edi_cfdi_invoice_try_send() invoice2._l10n_mx_edi_cfdi_invoice_try_send() bank_journal = self.env['account.journal'].create({ 'name': 'Bank 123456', 'code': 'BNK67', 'type': 'bank', 'bank_acc_number': '123456', 'currency_id': self.env.ref('base.USD').id, 'l10n_mx_edi_payment_method_id': self.env.ref('l10n_mx_edi.payment_method_transferencia').id, # To default to this payment method }) with self.mx_external_setup(date2): # Bank transaction 232 USD = 115.9942 MXN ≃ 115.99 MXN (1:2.0001) # At this rate 116 USD = 57.9971 MXN ≃ 58.00 MXN which is not half of 115.99 MXN # => create a 0.01 MXN auto balance line statement_line1 = self.env['account.bank.statement.line'].create({ 'journal_id': bank_journal.id, 'amount': 232.0, 'date': date2, 'payment_ref': 'test' }) # Open bank reconciliation widget wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=statement_line1.id).new({}) invoice_lines = (invoice1 | invoice2).line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable') wizard._action_add_new_amls(invoice_lines) self.assertRecordValues(wizard.line_ids, [ # pylint: disable=C0326 {'flag': 'liquidity', 'amount_currency': 232.0, 'currency_id': self.usd.id, 'balance': 115.99}, {'flag': 'new_aml', 'amount_currency': -116.0, 'currency_id': self.usd.id, 'balance': -77.34}, {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.usd.id, 'balance': 19.34}, {'flag': 'new_aml', 'amount_currency': -116.0, 'currency_id': self.usd.id, 'balance': -77.34}, {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.usd.id, 'balance': 19.34}, {'flag': 'auto_balance', 'amount_currency': 0.00, 'currency_id': self.usd.id, 'balance': 0.01}, ]) # Remove 0.01 in the balance of first exchange line first_exchange_line = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff')[:1] wizard._js_action_mount_line_in_edit(first_exchange_line.index) first_exchange_line.balance = 19.35 wizard._line_value_changed_balance(first_exchange_line) # Every line balance so no 'auto_balance' is generated self.assertRecordValues(wizard.line_ids, [ # pylint: disable=C0326 {'flag': 'liquidity', 'amount_currency': 232.0, 'currency_id': self.usd.id, 'balance': 115.99}, {'flag': 'new_aml', 'amount_currency': -116.0, 'currency_id': self.usd.id, 'balance': -77.34}, {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.usd.id, 'balance': 19.35}, {'flag': 'new_aml', 'amount_currency': -116.0, 'currency_id': self.usd.id, 'balance': -77.34}, {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.usd.id, 'balance': 19.34}, ]) self.assertRecordValues(wizard, [{'state': 'valid'}]) wizard._action_validate() self.assertRecordValues(statement_line1, [{'is_reconciled': True}]) self.assertRecordValues(invoice1, [{'payment_state': 'paid'}]) self.assertRecordValues(invoice2, [{'payment_state': 'paid'}]) with self.with_mocked_pac_sign_success(): statement_line1.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi(statement_line1.move_id, 'test_foreign_curr_statement_and_invoice_modify_exchange_move') def test_foreign_curr_payment_comp_curr_invoice_forced_balance(self): date1 = self.frozen_today - relativedelta(days=1) date2 = self.frozen_today self.setup_rates(self.chf, (date1, 16.0), (date2, 17.0)) with self.mx_external_setup(date1): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 1000.0, # = 62.5 CHF }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() with self.mx_external_setup(date2): payment = self.env['account.payment.register'] \ .with_context(active_model='account.move', active_ids=invoice.ids) \ .create({ 'payment_date': date2, 'currency_id': self.chf.id, 'amount': 62.0, # instead of 62.5 CHF 'payment_difference_handling': 'reconcile', 'writeoff_account_id': self.env.company.expense_currency_exchange_account_id.id, }) \ ._create_payments() with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi( payment.move_id, 'test_foreign_curr_payment_comp_curr_invoice_forced_balance', ) def test_comp_curr_payment_foreign_curr_invoice_forced_balance(self): date1 = self.frozen_today - relativedelta(days=1) date2 = self.frozen_today self.setup_rates(self.chf, (date1, 16.0), (date2, 17.0)) with self.mx_external_setup(date1): invoice = self._create_invoice( currency_id=self.chf.id, invoice_line_ids=[ Command.create({ 'product_id': self.product.id, 'price_unit': 62.5, # = 1000 MXN }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() with self.mx_external_setup(date2): payment = self.env['account.payment.register'] \ .with_context(active_model='account.move', active_ids=invoice.ids) \ .create({ 'payment_date': date2, 'currency_id': self.comp_curr.id, 'amount': 998.0, # instead of 1000.0 MXN 'payment_difference_handling': 'reconcile', 'writeoff_account_id': self.env.company.expense_currency_exchange_account_id.id, }) \ ._create_payments() with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() self._assert_invoice_payment_cfdi( payment.move_id, 'test_comp_curr_payment_foreign_curr_invoice_forced_balance', ) def test_cfdi_date_with_timezone(self): def assert_cfdi_date(document, tz, expected_datetime=None): self.assertTrue(document) cfdi_node = etree.fromstring(document.attachment_id.raw) cfdi_date_str = cfdi_node.get('Fecha') expected_datetime = (expected_datetime or self.frozen_today.astimezone(tz).replace(tzinfo=None)).replace(microsecond=0) current_date = datetime.strptime(cfdi_date_str, CFDI_DATE_FORMAT).replace(microsecond=0) self.assertEqual(current_date, expected_datetime) addresses = [ # America/Tijuana UTC-8 (-7 DST) { 'state_id': self.env.ref('base.state_mx_bc').id, 'zip': '22750', 'timezone': timezone('America/Tijuana'), }, # America/Bogota UTC-5 { 'state_id': self.env.ref('base.state_mx_q_roo').id, 'zip': '77890', 'timezone': timezone('America/Bogota'), }, # America/Boise UTC-7 (-6 DST) { 'state_id': self.env.ref('base.state_mx_chih').id, 'zip': '31820', 'timezone': timezone('America/Boise'), }, # America/Guatemala (Tiempo del centro areas) { 'state_id': self.env.ref('base.state_mx_nay').id, 'zip': '63726', 'timezone': timezone('America/Guatemala'), }, # America/Matamoros UTC-6 (-5 DST) { 'state_id': self.env.ref('base.state_mx_tamps').id, 'zip': '87300', 'timezone': timezone('America/Matamoros'), }, # Pacific area { 'state_id': self.env.ref('base.state_mx_son').id, 'zip': '83530', 'timezone': timezone('America/Hermosillo'), }, # America/Guatemala UTC-6 { 'state_id': self.env.ref('base.state_mx_ags').id, 'zip': '20914', 'timezone': timezone('America/Guatemala'), }, ] for address in addresses: tz = address.pop('timezone') with self.subTest(zip=address['zip']): self.env.company.partner_id.write(address) # Invoice on the future. with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_date=self.frozen_today + relativedelta(days=2), invoice_line_ids=[Command.create({'product_id': self.product.id})], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() document = invoice.l10n_mx_edi_invoice_document_ids.filtered(lambda x: x.state == 'invoice_sent')[:1] assert_cfdi_date(document, tz) # Invoice on the past. date_in_the_past = self.frozen_today - relativedelta(days=2) with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_date=date_in_the_past, invoice_line_ids=[Command.create({'product_id': self.product.id})], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() document = invoice.l10n_mx_edi_invoice_document_ids.filtered(lambda x: x.state == 'invoice_sent')[:1] assert_cfdi_date(document, tz, expected_datetime=date_in_the_past.replace(hour=23, minute=59, second=0)) with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice(invoice_line_ids=[Command.create({'product_id': self.product.id})]) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() document = invoice.l10n_mx_edi_invoice_document_ids.filtered(lambda x: x.state == 'invoice_sent')[:1] assert_cfdi_date(document, tz) # Test an immediate payment. payment = self.env['account.payment.register'] \ .with_context(active_model='account.move', active_ids=invoice.ids) \ .create({'amount': 100.0}) \ ._create_payments() with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() document = payment.l10n_mx_edi_payment_document_ids.filtered(lambda x: x.state == 'payment_sent')[:1] assert_cfdi_date(document, tz) # Test a payment made 10 days ago but send today. with self.mx_external_setup(self.frozen_today - relativedelta(days=10)): payment = self.env['account.payment.register'] \ .with_context(active_model='account.move', active_ids=invoice.ids) \ .create({'amount': 100.0}) \ ._create_payments() with self.mx_external_setup(self.frozen_today): with self.with_mocked_pac_sign_success(): payment.move_id._l10n_mx_edi_cfdi_payment_try_send() document = payment.l10n_mx_edi_payment_document_ids.filtered(lambda x: x.state == 'payment_sent')[:1] assert_cfdi_date(document, tz) def test_invoice_negative_lines_cfdi_amounts(self): """ Ensure the base amounts are correct in CFDI xml after dispatching the negative lines. """ product1 = self._create_product(name='product_1') product2 = self._create_product(name='product_2') product3 = self._create_product(name='product_3') downpayment = self._create_product(name='down_payment') with self.mx_external_setup(self.frozen_today): invoice = self._create_invoice( invoice_line_ids=[ Command.create({ 'product_id': product1.id, 'price_unit': 1000.0, 'quantity': 1.0, }), Command.create({ 'product_id': product2.id, 'price_unit': 1500.0, 'quantity': 1.0, }), Command.create({ 'product_id': product3.id, 'price_unit': 3000.0, 'quantity': 1.0, }), Command.create({ 'product_id': downpayment.id, 'price_unit': 4950.0, # It represents a 90% down payment 'quantity': -1.0, }), ], ) with self.with_mocked_pac_sign_success(): invoice._l10n_mx_edi_cfdi_invoice_try_send() # The down payment line should be dispatched to the 3 other lines, removing the 2 # biggest ones and generating a discount of 450 on the lowest one self._assert_invoice_cfdi(invoice, 'test_invoice_negative_lines_cfdi_amounts')