fix: add unique constraint on bin at db level

This commit is contained in:
Ankush Menat 2022-01-30 16:25:42 +05:30
parent d41f71acb7
commit 08810dbcce
4 changed files with 54 additions and 21 deletions

View File

@ -33,6 +33,7 @@
"oldfieldtype": "Link",
"options": "Warehouse",
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
@ -46,6 +47,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
@ -169,10 +171,11 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2021-03-30 23:09:39.572776",
"modified": "2022-01-30 17:04:54.715288",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@ -200,5 +203,6 @@
"quick_entry": 1,
"search_fields": "item_code,warehouse",
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@ -123,7 +123,7 @@ class Bin(Document):
self.db_set('projected_qty', self.projected_qty)
def on_doctype_update():
frappe.db.add_index("Bin", ["item_code", "warehouse"])
frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):

View File

@ -1,9 +1,36 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
# test_records = frappe.get_test_records('Bin')
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.utils import _create_bin
from erpnext.tests.utils import ERPNextTestCase
class TestBin(unittest.TestCase):
pass
class TestBin(ERPNextTestCase):
def test_concurrent_inserts(self):
""" Ensure no duplicates are possible in case of concurrent inserts"""
item_code = "_TestConcurrentBin"
make_item(item_code)
warehouse = "_Test Warehouse - _TC"
bin1 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
bin1.insert()
bin2 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
with self.assertRaises(frappe.UniqueValidationError):
bin2.insert()
# util method should handle it
bin = _create_bin(item_code, warehouse)
self.assertEqual(bin.item_code, item_code)
frappe.db.rollback()
def test_index_exists(self):
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
self.fail(f"Expected unique index on item-warehouse")

View File

@ -176,13 +176,7 @@ def get_latest_stock_balance():
def get_bin(item_code, warehouse):
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
if not bin:
bin_obj = frappe.get_doc({
"doctype": "Bin",
"item_code": item_code,
"warehouse": warehouse,
})
bin_obj.flags.ignore_permissions = 1
bin_obj.insert()
bin_obj = _create_bin(item_code, warehouse)
else:
bin_obj = frappe.get_doc('Bin', bin, for_update=True)
bin_obj.flags.ignore_permissions = True
@ -192,16 +186,24 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record:
bin_obj = frappe.get_doc({
"doctype": "Bin",
"item_code": item_code,
"warehouse": warehouse,
})
bin_obj = _create_bin(item_code, warehouse)
bin_record = bin_obj.name
return bin_record
def _create_bin(item_code, warehouse):
"""Create a bin and take care of concurrent inserts."""
bin_creation_savepoint = "create_bin"
try:
frappe.db.savepoint(bin_creation_savepoint)
bin_obj = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
bin_obj.flags.ignore_permissions = 1
bin_obj.insert()
bin_record = bin_obj.name
except frappe.UniqueValidationError:
frappe.db.rollback(save_point=bin_creation_savepoint) # preserve transaction in postgres
bin_obj = frappe.get_last_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
return bin_record
return bin_obj
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
"""WARNING: This function is deprecated. Inline this function instead of using it."""