mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-24 20:46:38 +00:00
fix(stock): support shared target UOM conversions (#58305)
This commit is contained in:
@@ -1510,7 +1510,7 @@ def get_uom_conv_factor(uom: str | None, stock_uom: str | None):
|
||||
inverse_match = frappe.db.get_value(
|
||||
"UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1
|
||||
)
|
||||
if inverse_match:
|
||||
if inverse_match and inverse_match.value:
|
||||
return flt(1 / inverse_match.value, frappe.get_precision("UOM Conversion Factor", "value"))
|
||||
|
||||
# This attempts to try and get conversion from intermediate UOM.
|
||||
@@ -1520,18 +1520,34 @@ def get_uom_conv_factor(uom: str | None, stock_uom: str | None):
|
||||
# therefore kg -> mg = 1000 / 0.001 = 1,000,000
|
||||
first = frappe.qb.DocType("UOM Conversion Factor").as_("first")
|
||||
second = frappe.qb.DocType("UOM Conversion Factor").as_("second")
|
||||
intermediate_match = (
|
||||
# Conversion pairs are not unique, so document names provide stable tie-breakers.
|
||||
shared_source_match = (
|
||||
frappe.qb.from_(first)
|
||||
.join(second)
|
||||
.on(first.from_uom == second.from_uom)
|
||||
.select((first.value / second.value).as_("value"))
|
||||
.where((first.to_uom == to_uom) & (second.to_uom == from_uom))
|
||||
.where((first.to_uom == to_uom) & (second.to_uom == from_uom) & (second.value != 0))
|
||||
.orderby(first.name, second.name)
|
||||
.limit(1)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
if intermediate_match:
|
||||
return flt(intermediate_match[0].value, frappe.get_precision("UOM Conversion Factor", "value"))
|
||||
if shared_source_match:
|
||||
return flt(shared_source_match[0].value, frappe.get_precision("UOM Conversion Factor", "value"))
|
||||
|
||||
shared_target_match = (
|
||||
frappe.qb.from_(first)
|
||||
.join(second)
|
||||
.on(first.to_uom == second.to_uom)
|
||||
.select((first.value / second.value).as_("value"))
|
||||
.where((first.from_uom == from_uom) & (second.from_uom == to_uom) & (second.value != 0))
|
||||
.orderby(first.name, second.name)
|
||||
.limit(1)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
if shared_target_match:
|
||||
return flt(shared_target_match[0].value, frappe.get_precision("UOM Conversion Factor", "value"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -71,6 +71,20 @@ def make_item(item_code=None, properties=None, uoms=None, barcode=None):
|
||||
return item
|
||||
|
||||
|
||||
def make_uom_conversion_factor(from_uom, to_uom, value, category="Mass"):
|
||||
for uom in (from_uom, to_uom):
|
||||
if not frappe.db.exists("UOM", uom):
|
||||
frappe.get_doc(doctype="UOM", uom_name=uom, category=category).insert()
|
||||
|
||||
return frappe.get_doc(
|
||||
doctype="UOM Conversion Factor",
|
||||
category=category,
|
||||
from_uom=from_uom,
|
||||
to_uom=to_uom,
|
||||
value=value,
|
||||
).insert()
|
||||
|
||||
|
||||
class TestItem(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -807,6 +821,32 @@ class TestItem(ERPNextTestSuite):
|
||||
factor = get_uom_conv_factor("Pound", "Gram")
|
||||
self.assertAlmostEqual(factor, 453.592, 3)
|
||||
|
||||
def test_uom_conv_intermediate_with_shared_target(self):
|
||||
make_uom_conversion_factor("_Test 3 Kg Bag", "Kg", 3)
|
||||
make_uom_conversion_factor("_Test 25 Kg Bag", "Kg", 25)
|
||||
|
||||
factor = get_uom_conv_factor("_Test 3 Kg Bag", "_Test 25 Kg Bag")
|
||||
|
||||
self.assertEqual(factor, 0.12)
|
||||
|
||||
def test_uom_conv_intermediate_with_shared_target_is_deterministic(self):
|
||||
make_uom_conversion_factor("_Test 3 Kg Bag", "Kg", 3)
|
||||
make_uom_conversion_factor("_Test 25 Kg Bag", "Kg", 25)
|
||||
make_uom_conversion_factor("_Test 3 Kg Bag", "Kg", 6)
|
||||
make_uom_conversion_factor("_Test 25 Kg Bag", "Kg", 20)
|
||||
|
||||
factor = get_uom_conv_factor("_Test 3 Kg Bag", "_Test 25 Kg Bag")
|
||||
|
||||
self.assertEqual(factor, 0.12)
|
||||
|
||||
def test_uom_conv_intermediate_with_shared_target_ignores_zero_divisor(self):
|
||||
make_uom_conversion_factor("_Test 3 Kg Bag", "Kg", 3)
|
||||
make_uom_conversion_factor("_Test 25 Kg Bag", "Kg", 0)
|
||||
|
||||
factor = get_uom_conv_factor("_Test 3 Kg Bag", "_Test 25 Kg Bag")
|
||||
|
||||
self.assertIsNone(factor)
|
||||
|
||||
def test_uom_conv_base_case(self):
|
||||
factor = get_uom_conv_factor("m", "m")
|
||||
self.assertEqual(factor, 1.0)
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item, make_uom_conversion_factor
|
||||
from erpnext.stock.doctype.material_request.mapper import make_purchase_order
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
@@ -28,6 +28,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
|
||||
make_serial_batch_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -4610,6 +4611,33 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(pr.items[0].conversion_factor, 1.0)
|
||||
|
||||
def test_conversion_factor_via_shared_target_uom(self):
|
||||
transaction_uom = "_Test 3 Kg Bag"
|
||||
stock_uom = "_Test 25 Kg Bag"
|
||||
make_uom_conversion_factor(transaction_uom, "Kg", 3)
|
||||
make_uom_conversion_factor(stock_uom, "Kg", 25)
|
||||
item = make_item("Test Item for Shared Target UOM", {"stock_uom": stock_uom})
|
||||
|
||||
conversion_factor = get_conversion_factor(item.name, transaction_uom).get("conversion_factor")
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item.name,
|
||||
qty=10,
|
||||
uom=transaction_uom,
|
||||
stock_uom=stock_uom,
|
||||
conversion_factor=conversion_factor,
|
||||
)
|
||||
|
||||
self.assertEqual(conversion_factor, 0.12)
|
||||
self.assertEqual(pr.items[0].stock_qty, 1.2)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": pr.doctype, "voucher_no": pr.name, "voucher_detail_no": pr.items[0].name},
|
||||
"actual_qty",
|
||||
),
|
||||
1.2,
|
||||
)
|
||||
|
||||
def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user