Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
1
purchase_unreconciled/tests/__init__.py
Executable file
1
purchase_unreconciled/tests/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import test_purchase_unreconciled
|
||||
429
purchase_unreconciled/tests/test_purchase_unreconciled.py
Executable file
429
purchase_unreconciled/tests/test_purchase_unreconciled.py
Executable file
@@ -0,0 +1,429 @@
|
||||
# Copyright 2019-21 ForgeFlow S.L.
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import exceptions, fields
|
||||
from odoo.tests import Form, SingleTransactionCase
|
||||
|
||||
|
||||
class TestPurchaseUnreconciled(SingleTransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.po_obj = cls.env["purchase.order"]
|
||||
cls.product_obj = cls.env["product.product"]
|
||||
cls.category_obj = cls.env["product.category"]
|
||||
cls.partner_obj = cls.env["res.partner"]
|
||||
cls.acc_obj = cls.env["account.account"]
|
||||
cls.account_move_obj = cls.env["account.move"]
|
||||
cls.company = cls.env.ref("base.main_company")
|
||||
cls.company.anglo_saxon_accounting = True
|
||||
expense_type = "expense"
|
||||
equity_type = "equity"
|
||||
asset_type = "asset_current"
|
||||
# Create partner:
|
||||
cls.partner = cls.partner_obj.create({"name": "Test Vendor"})
|
||||
# Create product that uses a reconcilable stock input account.
|
||||
cls.account = cls.acc_obj.create(
|
||||
{
|
||||
"name": "Test stock input account",
|
||||
"code": 9999,
|
||||
"account_type": asset_type,
|
||||
"reconcile": True,
|
||||
"company_ids": [(6, 0, [cls.company.id])],
|
||||
}
|
||||
)
|
||||
cls.writeoff_acc = cls.acc_obj.create(
|
||||
{
|
||||
"name": "Write-offf account",
|
||||
"code": 8888,
|
||||
"account_type": expense_type,
|
||||
"reconcile": True,
|
||||
"company_ids": [(6, 0, [cls.company.id])],
|
||||
}
|
||||
)
|
||||
cls.stock_journal = cls.env["account.journal"].create(
|
||||
{"name": "Stock Journal", "code": "STJTEST", "type": "general"}
|
||||
)
|
||||
# Create account for Goods Received Not Invoiced
|
||||
name = "Goods Received Not Invoiced"
|
||||
code = "grni"
|
||||
acc_type = equity_type
|
||||
cls.account_grni = cls._create_account(
|
||||
acc_type, name, code, cls.company, reconcile=True
|
||||
)
|
||||
# Create account for Cost of Goods Sold
|
||||
name = "Cost of Goods Sold"
|
||||
code = "cogs"
|
||||
acc_type = expense_type
|
||||
cls.account_cogs = cls._create_account(acc_type, name, code, cls.company)
|
||||
# Create account for Goods Delivered Not Invoiced
|
||||
name = "Goods Delivered Not Invoiced"
|
||||
code = "gdni"
|
||||
acc_type = expense_type
|
||||
cls.account_gdni = cls._create_account(
|
||||
acc_type, name, code, cls.company, reconcile=True
|
||||
)
|
||||
# Create account for Inventory
|
||||
name = "Inventory"
|
||||
code = "inventory"
|
||||
acc_type = asset_type
|
||||
cls.account_inventory = cls._create_account(acc_type, name, code, cls.company)
|
||||
cls.product_categ = cls.category_obj.create(
|
||||
{
|
||||
"name": "Test Category",
|
||||
"property_cost_method": "standard",
|
||||
"property_stock_valuation_account_id": cls.account_inventory.id,
|
||||
"property_stock_account_input_categ_id": cls.account_grni.id,
|
||||
"property_account_expense_categ_id": cls.account_cogs.id,
|
||||
"property_stock_account_output_categ_id": cls.account_gdni.id,
|
||||
"property_valuation": "real_time",
|
||||
"property_stock_journal": cls.stock_journal.id,
|
||||
}
|
||||
)
|
||||
cls.product_to_reconcile = cls.product_obj.create(
|
||||
{
|
||||
"name": "Purchased Product (To reconcile)",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
"standard_price": 100.0,
|
||||
"categ_id": cls.product_categ.id,
|
||||
}
|
||||
)
|
||||
cls.product_to_reconcile2 = cls.product_obj.create(
|
||||
{
|
||||
"name": "Purchased Product 2 (To reconcile)",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
"standard_price": 100.0,
|
||||
"categ_id": cls.product_categ.id,
|
||||
}
|
||||
)
|
||||
# Create PO's:
|
||||
cls.po = cls.po_obj.create(
|
||||
{
|
||||
"partner_id": cls.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": cls.product_to_reconcile.id,
|
||||
"name": cls.product_to_reconcile.name,
|
||||
"product_qty": 5.0,
|
||||
"price_unit": 100.0,
|
||||
"product_uom": cls.product_to_reconcile.uom_id.id,
|
||||
"date_planned": fields.Datetime.now(),
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
cls.po_2 = cls.po_obj.create(
|
||||
{
|
||||
"partner_id": cls.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": cls.product_to_reconcile.id,
|
||||
"name": cls.product_to_reconcile.name,
|
||||
"product_qty": 5.0,
|
||||
"price_unit": 100.0,
|
||||
"product_uom": cls.product_to_reconcile.uom_id.id,
|
||||
"date_planned": fields.Datetime.now(),
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
# company settings for automated valuation
|
||||
cls.company.purchase_lock_auto_reconcile = True
|
||||
cls.company.purchase_reconcile_account_id = cls.writeoff_acc
|
||||
cls.company.purchase_reconcile_journal_id = cls.stock_journal
|
||||
|
||||
@classmethod
|
||||
def _create_account(cls, acc_type, name, code, company, reconcile=False):
|
||||
"""Create an account."""
|
||||
account = cls.acc_obj.create(
|
||||
{
|
||||
"name": name,
|
||||
"code": code,
|
||||
"account_type": acc_type,
|
||||
"company_ids": [(6, 0, [company.id])],
|
||||
"reconcile": reconcile,
|
||||
}
|
||||
)
|
||||
return account
|
||||
|
||||
def _create_delivery(
|
||||
self,
|
||||
product,
|
||||
qty,
|
||||
):
|
||||
return self.env["stock.picking"].create(
|
||||
{
|
||||
"name": self.product_to_reconcile.name,
|
||||
"partner_id": self.partner.id,
|
||||
"picking_type_id": self.env.ref("stock.picking_type_out").id,
|
||||
"location_id": self.env.ref("stock.stock_location_stock").id,
|
||||
"location_dest_id": self.env.ref("stock.stock_location_customers").id,
|
||||
"move_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": self.product_to_reconcile.name,
|
||||
"product_id": self.product_to_reconcile.id,
|
||||
"product_uom": self.product_to_reconcile.uom_id.id,
|
||||
"product_uom_qty": qty,
|
||||
"location_id": self.env.ref(
|
||||
"stock.stock_location_stock"
|
||||
).id,
|
||||
"location_dest_id": self.env.ref(
|
||||
"stock.stock_location_customers"
|
||||
).id,
|
||||
"procure_method": "make_to_stock",
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def _do_picking(self, picking, date):
|
||||
"""Do picking with only one move on the given date."""
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
for move in picking.move_ids:
|
||||
move.quantity = move.product_uom_qty
|
||||
move.date = date
|
||||
picking.button_validate()
|
||||
|
||||
def test_01_nothing_to_reconcile(self):
|
||||
po = self.po
|
||||
self.assertEqual(po.state, "draft")
|
||||
po.button_confirm()
|
||||
self._do_picking(po.picking_ids, fields.Datetime.now())
|
||||
self.assertTrue(po.unreconciled)
|
||||
# Invoice created and validated:
|
||||
po.action_create_invoice()
|
||||
invoice_ids = po.invoice_ids.filtered(lambda i: i.move_type == "in_invoice")
|
||||
invoice_ids.invoice_date = datetime.now()
|
||||
invoice_ids.action_post()
|
||||
self.assertEqual(po.state, "purchase")
|
||||
# odoo does it automatically
|
||||
po._compute_unreconciled()
|
||||
self.assertFalse(po.unreconciled)
|
||||
|
||||
def test_03_search_unreconciled(self):
|
||||
"""Test searching unreconciled PO's."""
|
||||
po = self.po_2
|
||||
po.button_confirm()
|
||||
self._do_picking(po.picking_ids, fields.Datetime.now())
|
||||
res = self.po_obj.search([("unreconciled", "=", True)])
|
||||
po._compute_unreconciled()
|
||||
self.assertIn(po, res)
|
||||
self.assertNotIn(self.po, res)
|
||||
# Test value error:
|
||||
with self.assertRaises(ValueError):
|
||||
self.po_obj.search([("unreconciled", "=", "true")])
|
||||
|
||||
def test_04_action_reconcile(self):
|
||||
"""Test reconcile."""
|
||||
# Invoice created and validated:
|
||||
po = self.po_2
|
||||
self.assertTrue(po.unreconciled)
|
||||
po.action_create_invoice()
|
||||
invoice_form = Form(
|
||||
po.invoice_ids.filtered(lambda i: i.move_type == "in_invoice")[0]
|
||||
)
|
||||
# v14 reconciles automatically so here we force discrepancy
|
||||
# with invoice_form.edit(0) as inv_form:
|
||||
invoice_form.invoice_date = datetime.now()
|
||||
with invoice_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.price_unit = 99
|
||||
invoice = invoice_form.save()
|
||||
invoice.action_post()
|
||||
self.assertTrue(po.unreconciled)
|
||||
po.action_reconcile()
|
||||
po._compute_unreconciled()
|
||||
self.assertFalse(po.unreconciled)
|
||||
|
||||
def test_05_button_done_reconcile(self):
|
||||
"""Test auto reconcile when locking po."""
|
||||
po = self.po_2.copy()
|
||||
po.company_id.purchase_reconcile_account_id = self.writeoff_acc
|
||||
po.button_confirm()
|
||||
self._do_picking(po.picking_ids, fields.Datetime.now())
|
||||
# Invoice created and validated:
|
||||
# Odoo reconciles automatically so here we force discrepancy
|
||||
po.action_create_invoice()
|
||||
invoice_form = Form(
|
||||
po.invoice_ids.filtered(lambda i: i.move_type == "in_invoice")[0]
|
||||
)
|
||||
invoice_form.invoice_date = datetime.now()
|
||||
with invoice_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.price_unit = 99
|
||||
invoice = invoice_form.save()
|
||||
invoice.action_post()
|
||||
self.assertTrue(po.unreconciled)
|
||||
# check error if raised if not write-off account
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
self.company.purchase_reconcile_account_id = False
|
||||
po.button_done()
|
||||
# restore the write off account
|
||||
self.company.purchase_reconcile_account_id = self.writeoff_acc
|
||||
po.button_done()
|
||||
po._compute_unreconciled()
|
||||
self.assertFalse(po.unreconciled)
|
||||
|
||||
def test_06_dropship_not_reconcile_sale_journal_items(self):
|
||||
"""
|
||||
Create a fake dropship and lock the PO before receiving the customer
|
||||
invoice. The PO should not close the stock interim output account
|
||||
"""
|
||||
# to create the fake dropship we create a delivery and attach the
|
||||
# journals to the purchase order craeted later
|
||||
self.env["stock.quant"].create(
|
||||
{
|
||||
"product_id": self.product_to_reconcile.id,
|
||||
"location_id": self.env.ref("stock.stock_location_stock").id,
|
||||
"quantity": 1.0,
|
||||
}
|
||||
)
|
||||
delivery = self._create_delivery(self.product_to_reconcile, 1)
|
||||
self._do_picking(delivery, fields.Datetime.now())
|
||||
# We create the PO now and receive it
|
||||
po = self.po_2.copy()
|
||||
po.button_confirm()
|
||||
self._do_picking(po.picking_ids, fields.Datetime.now())
|
||||
self.assertTrue(po.unreconciled)
|
||||
# as long stock_dropshipping is not dependency, I force the PO to be in
|
||||
# the journal items of the delivery
|
||||
delivery_name = delivery.name
|
||||
delivery_ji = self.env["account.move.line"].search(
|
||||
[("move_id.ref", "=", delivery_name)]
|
||||
)
|
||||
delivery_ji.write(
|
||||
{"purchase_line_id": po.order_line[0], "purchase_order_id": po.id}
|
||||
)
|
||||
# then I lock the po to force reconciliation
|
||||
po.button_done()
|
||||
po._compute_unreconciled()
|
||||
self.assertFalse(po.unreconciled)
|
||||
# the PO is reconciled and the stock interim deliverd account is not
|
||||
# reconciled yet
|
||||
for jii in delivery_ji:
|
||||
self.assertFalse(jii.reconciled)
|
||||
|
||||
def test_07_multicompany(self):
|
||||
"""
|
||||
Force the company in the vendor bill to be wrong. The system will
|
||||
write-off the journals for the shipment because those are the only ones
|
||||
with the correct company
|
||||
"""
|
||||
po = self.po.copy()
|
||||
po.button_confirm()
|
||||
self._do_picking(po.picking_ids, fields.Datetime.now())
|
||||
# Invoice created and validated:
|
||||
f = Form(self.account_move_obj.with_context(default_move_type="in_invoice"))
|
||||
f.partner_id = po.partner_id
|
||||
f.invoice_date = fields.Date().today()
|
||||
f.purchase_vendor_bill_id = self.env["purchase.bill.union"].browse(-po.id)
|
||||
invoice = f.save()
|
||||
chicago_journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "chicago",
|
||||
"code": "ref",
|
||||
"type": "purchase",
|
||||
"company_id": self.ref("stock.res_company_1"),
|
||||
}
|
||||
)
|
||||
invoice.write(
|
||||
{
|
||||
"name": "/",
|
||||
}
|
||||
)
|
||||
invoice.write(
|
||||
{
|
||||
"company_id": self.ref("stock.res_company_1"),
|
||||
"journal_id": chicago_journal.id,
|
||||
}
|
||||
)
|
||||
invoice.action_post()
|
||||
self.assertEqual(po.state, "purchase")
|
||||
# The bill is wrong so this is unreconciled
|
||||
self.assertTrue(po.unreconciled)
|
||||
po.button_done()
|
||||
po._compute_unreconciled()
|
||||
self.assertFalse(po.unreconciled)
|
||||
# we check all the journals for the po have the same company
|
||||
ji = self.env["account.move.line"].search(
|
||||
[("purchase_order_id", "=", po.id), ("move_id", "!=", invoice.id)]
|
||||
)
|
||||
self.assertEqual(po.company_id, ji.mapped("company_id"))
|
||||
|
||||
def test_08_reconcile_by_product(self):
|
||||
"""
|
||||
Create a write-off by product
|
||||
"""
|
||||
po = self.po.copy()
|
||||
po.write(
|
||||
{
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product_to_reconcile2.id,
|
||||
"name": self.product_to_reconcile2.name,
|
||||
"product_qty": 5.0,
|
||||
"price_unit": 100.0,
|
||||
"product_uom": self.product_to_reconcile.uom_id.id,
|
||||
"date_planned": fields.Datetime.now(),
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
po.button_confirm()
|
||||
self._do_picking(po.picking_ids, fields.Datetime.now())
|
||||
# Invoice created and validated:
|
||||
f = Form(self.account_move_obj.with_context(default_move_type="in_invoice"))
|
||||
f.partner_id = po.partner_id
|
||||
f.invoice_date = fields.Date().today()
|
||||
f.purchase_vendor_bill_id = self.env["purchase.bill.union"].browse(-po.id)
|
||||
invoice = f.save()
|
||||
# force discrepancies
|
||||
with f.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.price_unit = 99
|
||||
with f.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.price_unit = 99
|
||||
invoice = f.save()
|
||||
invoice._post()
|
||||
# The bill is different price so this is unreconciled
|
||||
po._compute_unreconciled()
|
||||
self.assertTrue(po.unreconciled)
|
||||
po.button_done()
|
||||
po._compute_unreconciled()
|
||||
self.assertFalse(po.unreconciled)
|
||||
# we check all the journals are balanced by product
|
||||
ji_p1 = self.env["account.move.line"].search(
|
||||
[
|
||||
("purchase_order_id", "=", po.id),
|
||||
("product_id", "=", self.product_to_reconcile.id),
|
||||
("account_id", "=", self.account_grni.id),
|
||||
]
|
||||
)
|
||||
ji_p2 = self.env["account.move.line"].search(
|
||||
[
|
||||
("purchase_order_id", "=", po.id),
|
||||
("product_id", "=", self.product_to_reconcile2.id),
|
||||
("account_id", "=", self.account_grni.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(sum(ji_p1.mapped("balance")), 0.0)
|
||||
self.assertEqual(sum(ji_p2.mapped("balance")), 0.0)
|
||||
Reference in New Issue
Block a user