204 lines
8.8 KiB
Python
204 lines
8.8 KiB
Python
from freezegun import freeze_time
|
|
|
|
from odoo.addons.l10n_mx_edi.tests.common import EXTERNAL_MODE
|
|
from odoo.addons.l10n_mx_edi_extended.tests.common import TestMxExtendedEdiCommon
|
|
from odoo.addons.stock_landed_costs.tests.common import TestStockLandedCostsCommon
|
|
|
|
from odoo import Command
|
|
from odoo.tests import tagged
|
|
|
|
|
|
@tagged('post_install_l10n', 'post_install', '-at_install', *(['-standard', 'external'] if EXTERNAL_MODE else []))
|
|
class TestCFDIInvoiceLanding(TestMxExtendedEdiCommon, TestStockLandedCostsCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.env.user.group_ids = [Command.link(cls.env.ref("sales_team.group_sale_salesman").id)]
|
|
|
|
cls.picking_in_1 = cls.Picking.create({
|
|
'partner_id': cls.supplier_id,
|
|
'picking_type_id': cls.warehouse.in_type_id.id,
|
|
'location_id': cls.supplier_location_id,
|
|
'state': 'draft',
|
|
'location_dest_id': cls.warehouse.lot_stock_id.id,
|
|
})
|
|
cls.picking_in_2 = cls.Picking.create({
|
|
'partner_id': cls.supplier_id,
|
|
'picking_type_id': cls.warehouse.in_type_id.id,
|
|
'location_id': cls.supplier_location_id,
|
|
'state': 'draft',
|
|
'location_dest_id': cls.warehouse.lot_stock_id.id,
|
|
})
|
|
cls.product_customs = cls.env['product.product'].create({
|
|
'name': 'Test product template with customs',
|
|
'is_storable': True,
|
|
'list_price': 1000.0,
|
|
'categ_id': cls.stock_account_product_categ.id,
|
|
'unspsc_code_id': cls.env.ref('product_unspsc.unspsc_code_01010101').id,
|
|
'tracking': 'lot',
|
|
'l10n_mx_edi_use_customs_invoicing': True,
|
|
'invoice_policy': 'delivery',
|
|
'lot_valuated': True,
|
|
'taxes_id': [Command.link(cls.tax_16.id)],
|
|
})
|
|
|
|
cls.no_customs_product = cls.env['product.product'].create({
|
|
'name': 'Test product template without customs',
|
|
'is_storable': True,
|
|
'categ_id': cls.stock_account_product_categ.id,
|
|
'tracking': 'lot',
|
|
})
|
|
cls.Move.create([
|
|
{
|
|
'product_id': cls.product_customs.id,
|
|
'product_uom_qty': 10,
|
|
'product_uom': cls.product_customs.uom_id.id,
|
|
'picking_id': cls.picking_in_1.id,
|
|
'location_id': cls.supplier_location_id,
|
|
'location_dest_id': cls.warehouse.lot_stock_id.id
|
|
},
|
|
{
|
|
'product_id': cls.product_customs.id,
|
|
'product_uom_qty': 10,
|
|
'product_uom': cls.product_customs.uom_id.id,
|
|
'picking_id': cls.picking_in_2.id,
|
|
'location_id': cls.supplier_location_id,
|
|
'location_dest_id': cls.warehouse.lot_stock_id.id
|
|
},
|
|
])
|
|
|
|
def test_landed_cost_set_customs_number(self):
|
|
"""Test that landed cost is correctly set only on products with valid customs configuration
|
|
and that the name is computed correctly when setting and changing the landing cost"""
|
|
lot_name = "TESTLOT01"
|
|
self.Move.create({
|
|
'product_id': self.no_customs_product.id,
|
|
'product_uom_qty': 10,
|
|
'product_uom': self.no_customs_product.uom_id.id,
|
|
'picking_id': self.picking_in_1.id,
|
|
'location_id': self.supplier_location_id,
|
|
'location_dest_id': self.warehouse.lot_stock_id.id
|
|
})
|
|
self.picking_in_1.action_confirm()
|
|
self.picking_in_1.move_line_ids.write({'lot_name': lot_name})
|
|
self.picking_in_1.button_validate()
|
|
|
|
landing_cost = self.env['stock.landed.cost'].create({
|
|
'l10n_mx_edi_customs_number': '15 48 3009 0001234',
|
|
'picking_ids': [Command.link(self.picking_in_1.id)],
|
|
'account_journal_id': self.company_data['default_journal_misc'].id,
|
|
})
|
|
|
|
landing_cost.button_validate()
|
|
lot_with_customs = self.picking_in_1.move_line_ids.lot_id.filtered(
|
|
lambda l: l.l10n_mx_edi_customs_number and l.product_id.l10n_mx_edi_use_customs_invoicing
|
|
)
|
|
|
|
self.assertEqual(len(lot_with_customs), 1)
|
|
self.assertRecordValues(lot_with_customs, [{
|
|
"l10n_mx_edi_landed_cost_id": landing_cost.id,
|
|
"name": f"{lot_name} / {landing_cost.l10n_mx_edi_customs_number}",
|
|
}])
|
|
|
|
other_landing_cost = self.env['stock.landed.cost'].create({
|
|
'l10n_mx_edi_customs_number': '15 48 3009 0001235',
|
|
'picking_ids': [Command.link(self.picking_in_1.id)],
|
|
'account_journal_id': self.company_data['default_journal_misc'].id,
|
|
})
|
|
other_landing_cost.button_validate()
|
|
|
|
# If we change the landed cost, the customs number should change which means the name too.
|
|
lot_with_customs.l10n_mx_edi_landed_cost_id = other_landing_cost
|
|
self.assertEqual(lot_with_customs.name, f"{lot_name} / {other_landing_cost.l10n_mx_edi_customs_number}")
|
|
|
|
lot_with_customs.l10n_mx_edi_landed_cost_id = False
|
|
self.assertEqual(lot_with_customs.name, lot_name)
|
|
|
|
def test_invoice_cfdi_customs(self):
|
|
with self.mx_external_setup(self.frozen_today):
|
|
pickings = self.picking_in_1 | self.picking_in_2
|
|
pickings.action_confirm()
|
|
self.picking_in_1.move_line_ids.write({
|
|
"lot_name": "LOT001",
|
|
})
|
|
self.picking_in_2.move_line_ids.write({
|
|
"lot_name": "LOT002",
|
|
})
|
|
pickings.button_validate()
|
|
with freeze_time("2025-08-15"):
|
|
landed_costs = self.env['stock.landed.cost'].create([
|
|
{
|
|
'l10n_mx_edi_customs_number': '15 48 3009 0001234',
|
|
'picking_ids': [Command.link(self.picking_in_1.id)],
|
|
'account_journal_id': self.company_data['default_journal_misc'].id,
|
|
},
|
|
{
|
|
'l10n_mx_edi_customs_number': '15 48 3009 0001235',
|
|
'picking_ids': [Command.link(self.picking_in_2.id)],
|
|
'account_journal_id': self.company_data['default_journal_misc'].id,
|
|
}
|
|
]).sorted("l10n_mx_edi_customs_number")
|
|
|
|
landed_costs.button_validate()
|
|
|
|
sale = self.env['sale.order'].create({
|
|
'partner_id': self.partner_mx.id,
|
|
'order_line': [
|
|
(0, 0, {
|
|
'name': self.product_customs.name,
|
|
'product_id': self.product_customs.id,
|
|
'product_uom_qty': 20,
|
|
'price_unit': 1000,
|
|
})
|
|
],
|
|
})
|
|
|
|
# New workflow should work even if we disabled config since the line is already created
|
|
self.product_customs.l10n_mx_edi_use_customs_invoicing = False
|
|
|
|
sale.action_confirm()
|
|
picking_sale = sale.picking_ids
|
|
picking_sale.action_confirm()
|
|
picking_sale.button_validate()
|
|
# A sales user should be able to create the invoice without triggering an access error
|
|
invoice_1 = sale._create_invoices()
|
|
|
|
# Two invoice lines should have been created with
|
|
# '15 48 3009 0001234' and '15 48 3009 0001235' customs numbers and 10 quantity each
|
|
self.assertEqual(len(invoice_1.invoice_line_ids), 2)
|
|
invoice_lines = invoice_1.invoice_line_ids.sorted('l10n_mx_edi_customs_number')
|
|
self.assertRecordValues(invoice_lines, [
|
|
{
|
|
"l10n_mx_edi_customs_number": "15 48 3009 0001234",
|
|
"quantity": 10.0,
|
|
},
|
|
{
|
|
"l10n_mx_edi_customs_number": "15 48 3009 0001235",
|
|
"quantity": 10.0,
|
|
}
|
|
])
|
|
|
|
invoice_lines[0].quantity = 2
|
|
invoice_1.action_post()
|
|
invoice_2 = sale._create_invoices()
|
|
|
|
# Since we reduced the quantity before, there should be 8 units to invoice.
|
|
# Only one line should be created for customs '15 48 3009 0001234' for that quantity
|
|
self.assertEqual(len(invoice_2.invoice_line_ids), 1)
|
|
self.assertRecordValues(invoice_2.invoice_line_ids, [{
|
|
"l10n_mx_edi_customs_number": "15 48 3009 0001234",
|
|
"quantity": 8,
|
|
}])
|
|
|
|
invoice_2.action_post()
|
|
|
|
with self.with_mocked_pac_sign_success():
|
|
invoice_1._l10n_mx_edi_cfdi_invoice_try_send()
|
|
self._assert_invoice_cfdi(invoice_1, 'test_invoice_cfdi_1_customs')
|
|
|
|
with self.with_mocked_pac_sign_success():
|
|
invoice_2._l10n_mx_edi_cfdi_invoice_try_send()
|
|
self._assert_invoice_cfdi(invoice_2, 'test_invoice_cfdi_2_customs')
|