[enhance] automatic batch selection in Delivery Note and Stock Entry
This commit is contained in:
parent
e385b5b97b
commit
551406ab11
@ -177,17 +177,18 @@ class StockController(AccountsController):
|
||||
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
|
||||
return stock_ledger
|
||||
|
||||
def make_batches(self):
|
||||
def make_batches(self, warehouse_field):
|
||||
'''Create batches if required. Called before submit'''
|
||||
for d in self.items:
|
||||
has_batch_no, create_new_batch = frappe.db.get_value('Item', d.item_code, ['has_batch_no', 'create_new_batch'])
|
||||
if has_batch_no and not d.batch_no and create_new_batch:
|
||||
d.batch_no = frappe.get_doc(dict(
|
||||
doctype='Batch',
|
||||
item=d.item_code,
|
||||
supplier=getattr(self, 'supplier', None),
|
||||
reference_doctype=self.doctype,
|
||||
reference_name=self.name)).insert().name
|
||||
if d.get(warehouse_field) and not d.batch_no:
|
||||
has_batch_no, create_new_batch = frappe.db.get_value('Item', d.item_code, ['has_batch_no', 'create_new_batch'])
|
||||
if has_batch_no and create_new_batch:
|
||||
d.batch_no = frappe.get_doc(dict(
|
||||
doctype='Batch',
|
||||
item=d.item_code,
|
||||
supplier=getattr(self, 'supplier', None),
|
||||
reference_doctype=self.doctype,
|
||||
reference_name=self.name)).insert().name
|
||||
|
||||
def make_adjustment_entry(self, expected_gle, voucher_obj):
|
||||
from erpnext.accounts.utils import get_stock_and_account_difference
|
||||
|
@ -6,6 +6,8 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class UnableToSelectBatchError(frappe.ValidationError): pass
|
||||
|
||||
class Batch(Document):
|
||||
def autoname(self):
|
||||
'''Generate random ID for batch if not specified'''
|
||||
@ -34,8 +36,15 @@ class Batch(Document):
|
||||
frappe.throw(_("The selected item cannot have Batch"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_qty(batch_no, warehouse=None):
|
||||
'''Returns batch actual qty if warehouse is passed, or returns dict of qty by warehouse if warehouse is None'''
|
||||
def get_batch_qty(batch_no=None, warehouse=None, item_code=None):
|
||||
'''Returns batch actual qty if warehouse is passed,
|
||||
or returns dict of qty by warehouse if warehouse is None
|
||||
|
||||
The user must pass either batch_no or batch_no + warehouse or item_code + warehouse
|
||||
|
||||
:param batch_no: Optional - give qty for this batch no
|
||||
:param warehouse: Optional - give qty for this warehouse
|
||||
:param item_code: Optional - give qty for this item'''
|
||||
frappe.has_permission('Batch', throw=True)
|
||||
out = 0
|
||||
if batch_no and warehouse:
|
||||
@ -48,6 +57,11 @@ def get_batch_qty(batch_no, warehouse=None):
|
||||
from `tabStock Ledger Entry`
|
||||
where batch_no=%s
|
||||
group by warehouse''', batch_no, as_dict=1)
|
||||
if not batch_no and item_code and warehouse:
|
||||
out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %s and warehouse=%s
|
||||
group by batch_no''', (item_code, warehouse), as_dict=1)
|
||||
return out
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -76,3 +90,30 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id = None):
|
||||
stock_entry.submit()
|
||||
|
||||
return batch.name
|
||||
|
||||
def set_batch_nos(doc, warehouse_field, throw = False):
|
||||
'''Automatically select `batch_no` for outgoing items in item table'''
|
||||
for d in doc.items:
|
||||
has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no')
|
||||
warehouse = d.get(warehouse_field, None)
|
||||
if has_batch_no and not d.batch_no and warehouse:
|
||||
d.batch_no = get_batch_no(d.item_code, warehouse, d.qty, throw)
|
||||
|
||||
def get_batch_no(item_code, warehouse, qty, throw=False):
|
||||
'''get the smallest batch with for the given item_code, warehouse and qty'''
|
||||
batches = sorted(
|
||||
get_batch_qty(item_code = item_code, warehouse = warehouse),
|
||||
lambda a, b: 1 if a.qty > b.qty else -1)
|
||||
|
||||
batch_no = None
|
||||
for b in batches:
|
||||
if b.qty >= qty:
|
||||
batch_no = b.batch_no
|
||||
# found!
|
||||
break
|
||||
|
||||
if not batch_no:
|
||||
frappe.msgprint(_('Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement').format(frappe.bold(item_code)))
|
||||
if throw: raise UnableToSelectBatchError
|
||||
|
||||
return batch_no
|
@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.exceptions import ValidationError
|
||||
import unittest
|
||||
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError
|
||||
|
||||
class TestBatch(unittest.TestCase):
|
||||
def test_item_has_batch_enabled(self):
|
||||
@ -21,7 +21,7 @@ class TestBatch(unittest.TestCase):
|
||||
if not frappe.db.exists('ITEM-BATCH-1'):
|
||||
make_item('ITEM-BATCH-1', dict(has_batch_no = 1, create_new_batch = 1))
|
||||
|
||||
def test_purchase_receipt(self):
|
||||
def test_purchase_receipt(self, batch_qty = 100):
|
||||
'''Test automated batch creation from Purchase Receipt'''
|
||||
self.make_batch_item()
|
||||
|
||||
@ -31,7 +31,7 @@ class TestBatch(unittest.TestCase):
|
||||
items = [
|
||||
dict(
|
||||
item_code = 'ITEM-BATCH-1',
|
||||
qty = 100,
|
||||
qty = batch_qty,
|
||||
rate = 10
|
||||
)
|
||||
]
|
||||
@ -39,11 +39,12 @@ class TestBatch(unittest.TestCase):
|
||||
receipt.submit()
|
||||
|
||||
self.assertTrue(receipt.items[0].batch_no)
|
||||
self.assertEquals(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 100)
|
||||
self.assertEquals(get_batch_qty(receipt.items[0].batch_no,
|
||||
receipt.items[0].warehouse), batch_qty)
|
||||
|
||||
return receipt
|
||||
|
||||
def test_stock_entry(self):
|
||||
def test_stock_entry_incoming(self):
|
||||
'''Test batch creation via Stock Entry (Production Order)'''
|
||||
|
||||
self.make_batch_item()
|
||||
@ -67,6 +68,78 @@ class TestBatch(unittest.TestCase):
|
||||
self.assertTrue(stock_entry.items[0].batch_no)
|
||||
self.assertEquals(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90)
|
||||
|
||||
def test_delivery_note(self):
|
||||
'''Test automatic batch selection for outgoing items'''
|
||||
batch_qty = 15
|
||||
receipt = self.test_purchase_receipt(batch_qty)
|
||||
|
||||
delivery_note = frappe.get_doc(dict(
|
||||
doctype = 'Delivery Note',
|
||||
customer = '_Test Customer',
|
||||
company = receipt.company,
|
||||
items = [
|
||||
dict(
|
||||
item_code = 'ITEM-BATCH-1',
|
||||
qty = batch_qty,
|
||||
rate = 10,
|
||||
warehouse = receipt.items[0].warehouse
|
||||
)
|
||||
]
|
||||
)).insert()
|
||||
delivery_note.submit()
|
||||
|
||||
# shipped with same batch
|
||||
self.assertEquals(delivery_note.items[0].batch_no, receipt.items[0].batch_no)
|
||||
|
||||
# balance is 0
|
||||
self.assertEquals(get_batch_qty(receipt.items[0].batch_no,
|
||||
receipt.items[0].warehouse), 0)
|
||||
|
||||
def test_delivery_note_fail(self):
|
||||
'''Test automatic batch selection for outgoing items'''
|
||||
receipt = self.test_purchase_receipt(100)
|
||||
delivery_note = frappe.get_doc(dict(
|
||||
doctype = 'Delivery Note',
|
||||
customer = '_Test Customer',
|
||||
company = receipt.company,
|
||||
items = [
|
||||
dict(
|
||||
item_code = 'ITEM-BATCH-1',
|
||||
qty = 5000,
|
||||
rate = 10,
|
||||
warehouse = receipt.items[0].warehouse
|
||||
)
|
||||
]
|
||||
))
|
||||
self.assertRaises(UnableToSelectBatchError, delivery_note.insert)
|
||||
|
||||
def test_stock_entry_outgoing(self):
|
||||
'''Test automatic batch selection for outgoing stock entry'''
|
||||
|
||||
batch_qty = 16
|
||||
receipt = self.test_purchase_receipt(batch_qty)
|
||||
|
||||
stock_entry = frappe.get_doc(dict(
|
||||
doctype = 'Stock Entry',
|
||||
purpose = 'Material Issue',
|
||||
company = receipt.company,
|
||||
items = [
|
||||
dict(
|
||||
item_code = 'ITEM-BATCH-1',
|
||||
qty = batch_qty,
|
||||
s_warehouse = receipt.items[0].warehouse,
|
||||
)
|
||||
]
|
||||
)).insert()
|
||||
stock_entry.submit()
|
||||
|
||||
# assert same batch is selected
|
||||
self.assertEqual(stock_entry.items[0].batch_no, receipt.items[0].batch_no)
|
||||
|
||||
# balance is 0
|
||||
self.assertEquals(get_batch_qty(receipt.items[0].batch_no,
|
||||
receipt.items[0].warehouse), 0)
|
||||
|
||||
def test_batch_split(self):
|
||||
'''Test batch splitting'''
|
||||
receipt = self.test_purchase_receipt()
|
||||
|
@ -11,7 +11,7 @@ import frappe.defaults
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@ -106,6 +106,9 @@ class DeliveryNote(SellingController):
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_with_previous_doc()
|
||||
|
||||
if self._action != 'submit':
|
||||
set_batch_nos(self, 'warehouse', True)
|
||||
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
|
||||
|
@ -51,7 +51,7 @@ class PurchaseReceipt(BuyingController):
|
||||
super(PurchaseReceipt, self).validate()
|
||||
|
||||
if self._action=="submit":
|
||||
self.make_batches()
|
||||
self.make_batches('warehouse')
|
||||
else:
|
||||
self.set_status()
|
||||
|
||||
|
@ -9,6 +9,7 @@ from frappe.utils import cstr, cint, flt, comma_or, getdate, nowdate, formatdate
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_no, set_batch_nos
|
||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||
import json
|
||||
|
||||
@ -49,7 +50,9 @@ class StockEntry(StockController):
|
||||
self.validate_batch()
|
||||
|
||||
if self._action == 'submit':
|
||||
self.make_batches()
|
||||
self.make_batches('t_warehouse')
|
||||
else:
|
||||
set_batch_nos(self, 's_warehouse', True)
|
||||
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount(update_finished_item_rate=False)
|
||||
@ -89,8 +92,10 @@ class StockEntry(StockController):
|
||||
if item.item_code not in stock_items:
|
||||
frappe.throw(_("{0} is not a stock Item").format(item.item_code))
|
||||
|
||||
item_details = self.get_item_details(frappe._dict({"item_code": item.item_code,
|
||||
"company": self.company, "project": self.project, "uom": item.uom}), for_update=True)
|
||||
item_details = self.get_item_details(frappe._dict(
|
||||
{"item_code": item.item_code, "company": self.company,
|
||||
"project": self.project, "uom": item.uom, 's_warehouse': item.s_warehouse}),
|
||||
for_update=True)
|
||||
|
||||
for f in ("uom", "stock_uom", "description", "item_name", "expense_account",
|
||||
"cost_center", "conversion_factor"):
|
||||
@ -465,7 +470,9 @@ class StockEntry(StockController):
|
||||
|
||||
def get_item_details(self, args=None, for_update=False):
|
||||
item = frappe.db.sql("""select stock_uom, description, image, item_name,
|
||||
expense_account, buying_cost_center, item_group from `tabItem`
|
||||
expense_account, buying_cost_center, item_group, has_serial_no,
|
||||
has_batch_no
|
||||
from `tabItem`
|
||||
where name = %s
|
||||
and disabled=0
|
||||
and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s)""",
|
||||
@ -475,7 +482,7 @@ class StockEntry(StockController):
|
||||
|
||||
item = item[0]
|
||||
|
||||
ret = {
|
||||
ret = frappe._dict({
|
||||
'uom' : item.stock_uom,
|
||||
'stock_uom' : item.stock_uom,
|
||||
'description' : item.description,
|
||||
@ -489,8 +496,10 @@ class StockEntry(StockController):
|
||||
'batch_no' : '',
|
||||
'actual_qty' : 0,
|
||||
'basic_rate' : 0,
|
||||
'serial_no' : ''
|
||||
}
|
||||
'serial_no' : '',
|
||||
'has_serial_no' : item.has_serial_no,
|
||||
'has_batch_no' : item.has_batch_no
|
||||
})
|
||||
for d in [["Account", "expense_account", "default_expense_account"],
|
||||
["Cost Center", "cost_center", "cost_center"]]:
|
||||
company = frappe.db.get_value(d[0], ret.get(d[1]), "company")
|
||||
@ -510,6 +519,11 @@ class StockEntry(StockController):
|
||||
stock_and_rate = args.get('warehouse') and get_warehouse_details(args) or {}
|
||||
ret.update(stock_and_rate)
|
||||
|
||||
# automatically select batch for outgoing item
|
||||
if (args.get('s_warehouse', None) and args.get('qty') and
|
||||
ret.get('has_batch_no') and not args.get('batch_no')):
|
||||
args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty'])
|
||||
|
||||
return ret
|
||||
|
||||
def get_items(self):
|
||||
|
@ -813,7 +813,7 @@
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
@ -1040,11 +1040,11 @@
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldname": "is_sample_item",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
@ -1053,7 +1053,7 @@
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
"label": "Is Sample Item",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
@ -1225,7 +1225,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-04-19 11:54:31.645381",
|
||||
"modified": "2017-04-21 02:56:48.306626",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
@ -9,6 +9,7 @@ import json
|
||||
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item, set_transaction_type
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from frappe.model.meta import get_field_precision
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_no
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(args):
|
||||
@ -74,7 +75,12 @@ def get_item_details(args):
|
||||
out.update(get_pricing_rule_for_item(args))
|
||||
|
||||
if args.get("doctype") in ("Sales Invoice", "Delivery Note") and out.stock_qty > 0:
|
||||
out.serial_no = get_serial_no(out)
|
||||
if out.has_serial_no:
|
||||
out.serial_no = get_serial_no(out)
|
||||
|
||||
if out.has_batch_no:
|
||||
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
|
||||
|
||||
|
||||
if args.transaction_date and item.lead_time_days:
|
||||
out.schedule_date = out.lead_time_date = add_days(args.transaction_date,
|
||||
@ -154,6 +160,8 @@ def get_basic_details(args, item):
|
||||
"income_account": get_default_income_account(args, item),
|
||||
"expense_account": get_default_expense_account(args, item),
|
||||
"cost_center": get_default_cost_center(args, item),
|
||||
'has_serial_no': item.has_serial_no,
|
||||
'has_batch_no': item.has_batch_no,
|
||||
"batch_no": None,
|
||||
"item_tax_rate": json.dumps(dict(([d.tax_type, d.tax_rate] for d in
|
||||
item.get("taxes")))),
|
||||
|
Loading…
Reference in New Issue
Block a user