[New Feature] Sample Retention from a Batch of item recieved (#11624)

This commit is contained in:
KanchanChauhan 2017-12-06 18:38:01 +05:30 committed by Nabin Hait
parent 5c62368a65
commit f6aff3de96
15 changed files with 1680 additions and 1211 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -16,3 +16,4 @@ purchase-return
articles
opening-stock
stock-how-to
retain-sample-stock

View File

@ -0,0 +1,26 @@
# Retain Sample Stock
A sample of a batch of starting material, packaging material or finished product is stored for the purpose of being analyzed should the need arise later.
### Set Sample Retention Warehouse in Stock Settings
It is advised to create a new warehouse just for retaining samples and not use it in production.
<img class="screenshot" alt="Sample Retention Warehouse" src="/docs/assets/img/stock/sample-warehouse.png">
### Enable Retain Sample in Item master
Check Retain Sample and Maximum allowed samples in Item Master for a batch. Please note that Retain Sample is based
on Batch hence Has Batch No should be enabled as well.
<img class="screenshot" alt="Retain Sample" src="/docs/assets/img/stock/retain-sample.png">
### Stock Entry
Whenever a Stock Entry is created with the purpose as Material Receipt, for items which have Retain Sample enabled, the Sample Quantity can be set during that Stock Entry. Sample quantity cannot be more than the Maximum sample quantity set in Item Master.
<img class="screenshot" alt="Retain Sample" src="/docs/assets/img/stock/material-receipt-sample.png">
On submission of this Stock Entry, button 'Make Retention Stock Entry' will be available to make another Stock Entry for the transfer of sample items from the mentioned batch to the retention warehouse set in Stock Settings. On clicking this button it will direct you to new Stock Entry with all the information, verify the information and click Submit.
<img class="screenshot" alt="Retain Sample" src="/docs/assets/img/stock/material-transfer-sample.png">

View File

@ -1183,6 +1183,38 @@
"set_only_once": 1,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "has_batch_no",
"description": "",
"fieldname": "create_new_batch",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Automatically Create New Batch",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -1221,8 +1253,7 @@
"collapsible": 0,
"columns": 0,
"depends_on": "has_batch_no",
"description": "",
"fieldname": "create_new_batch",
"fieldname": "retain_sample",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
@ -1231,7 +1262,39 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Automatically Create New Batch",
"label": "Retain Sample",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: (doc.retain_sample && doc.has_batch_no)",
"description": "Maximum sample quantity that can be retained",
"fieldname": "sample_quantity",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Max Sample Quantity",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -3360,7 +3423,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 1,
"modified": "2017-11-20 12:18:07.259756",
"modified": "2017-12-04 15:37:58.413290",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@ -97,6 +97,7 @@ class Item(WebsiteGenerator):
self.validate_website_image()
self.make_thumbnail()
self.validate_fixed_asset()
self.validate_retain_sample()
if not self.get("__islocal"):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@ -256,6 +257,12 @@ class Item(WebsiteGenerator):
if asset:
frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item'))
def validate_retain_sample(self):
if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'):
frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first"));
if self.retain_sample and not self.has_batch_no:
frappe.throw(_(" {0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(self.item_code))
def get_context(self, context):
context.show_search=True
context.search_link = '/product_search'

View File

@ -98,6 +98,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
if(flt(this.frm.doc.per_billed) < 100) {
cur_frm.add_custom_button(__('Invoice'), this.make_purchase_invoice, __("Make"));
}
cur_frm.add_custom_button(__('Retention Stock Entry'), this.make_retention_stock_entry, __("Make"));
if(!this.frm.doc.subscription) {
cur_frm.add_custom_button(__('Subscription'), function() {
@ -137,7 +138,26 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
reopen_purchase_receipt: function() {
cur_frm.cscript.update_status("Submitted");
}
},
make_retention_stock_entry: function() {
frappe.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.move_sample_to_retention_warehouse",
args:{
"company": cur_frm.doc.company,
"items": cur_frm.doc.items
},
callback: function (r) {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
else {
frappe.msgprint(__("Retention Stock Entry already created or Sample Quantity not provided"));
}
}
});
},
});
@ -206,3 +226,36 @@ frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) {
}
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes");
});
frappe.ui.form.on('Purchase Receipt Item', {
item_code: function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
frappe.db.get_value('Item', {name: d.item_code}, 'sample_quantity', (r) => {
frappe.model.set_value(cdt, cdn, "sample_quantity", r.sample_quantity);
});
},
sample_quantity: function(frm, cdt, cdn) {
validate_sample_quantity(frm, cdt, cdn);
},
batch_no: function(frm, cdt, cdn) {
validate_sample_quantity(frm, cdt, cdn);
},
});
var validate_sample_quantity = function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.sample_quantity) {
frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.validate_sample_quantity',
args: {
batch_no: d.batch_no,
item_code: d.item_code,
sample_quantity: d.sample_quantity,
qty: d.qty
},
callback: (r) => {
frappe.model.set_value(cdt, cdn, "sample_quantity", r.message);
}
});
}
};

View File

@ -607,6 +607,69 @@
"unique": 0,
"width": "100px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "retain_sample",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Retain Sample",
"length": 0,
"no_copy": 0,
"options": "item_code.retain_sample",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "retain_sample",
"fieldname": "sample_quantity",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sample Quantity",
"length": 0,
"no_copy": 0,
"options": "item_code.sample_quantity",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -2227,7 +2290,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-11-30 14:19:14.276376",
"modified": "2017-12-06 13:50:08.201145",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@ -86,6 +86,12 @@ frappe.ui.form.on('Stock Entry', {
if(frm.doc.company) {
frm.trigger("toggle_display_account_head");
}
if(frm.doc.docstatus==1 && frm.doc.purpose == "Material Receipt") {
frm.add_custom_button(__('Make Retention Stock Entry'), function () {
frm.trigger("make_retention_stock_entry");
});
}
},
purpose: function(frm) {
@ -122,6 +128,25 @@ frappe.ui.form.on('Stock Entry', {
});
},
make_retention_stock_entry: function(frm) {
frappe.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.move_sample_to_retention_warehouse",
args:{
"company": frm.doc.company,
"items": frm.doc.items
},
callback: function (r) {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
else {
frappe.msgprint(__("Retention Stock Entry already created or Sample Quantity not provided"));
}
}
});
},
toggle_display_account_head: function(frm) {
var enabled = erpnext.is_perpetual_inventory_enabled(frm.doc.company);
frm.fields_dict["items"].grid.set_column_disp(["cost_center", "expense_account"], enabled);
@ -327,9 +352,33 @@ frappe.ui.form.on('Stock Entry Detail', {
},
cost_center: function(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_row(frm.doc, cdt, cdn, "items", "cost_center");
}
},
sample_quantity: function(frm, cdt, cdn) {
validate_sample_quantity(frm, cdt, cdn);
},
batch_no: function(frm, cdt, cdn) {
validate_sample_quantity(frm, cdt, cdn);
},
});
var validate_sample_quantity = function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.sample_quantity && frm.doc.purpose == "Material Receipt") {
frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.validate_sample_quantity',
args: {
batch_no: d.batch_no,
item_code: d.item_code,
sample_quantity: d.sample_quantity,
qty: d.transfer_qty
},
callback: (r) => {
frappe.model.set_value(cdt, cdn, "sample_quantity", r.message);
}
});
}
};
frappe.ui.form.on('Landed Cost Taxes and Charges', {
amount: function(frm) {
frm.events.calculate_amount(frm);
@ -575,6 +624,8 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
this.frm.fields_dict["items"].grid.set_column_disp("s_warehouse", doc.purpose!='Material Receipt');
this.frm.fields_dict["items"].grid.set_column_disp("t_warehouse", doc.purpose!='Material Issue');
this.frm.fields_dict["items"].grid.set_column_disp("retain_sample", doc.purpose=='Material Receipt');
this.frm.fields_dict["items"].grid.set_column_disp("sample_quantity", doc.purpose=='Material Receipt');
this.frm.cscript.toggle_enable_bom();

View File

@ -9,13 +9,14 @@ 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.stock.doctype.batch.batch import get_batch_no, set_batch_nos, get_batch_qty
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
import json
class IncorrectValuationRateError(frappe.ValidationError): pass
class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass
class OperationsNotCompleteError(frappe.ValidationError): pass
class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
from erpnext.controllers.stock_controller import StockController
@ -472,7 +473,7 @@ 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, has_serial_no,
has_batch_no
has_batch_no, sample_quantity
from `tabItem`
where name = %s
and disabled=0
@ -499,7 +500,8 @@ class StockEntry(StockController):
'basic_rate' : 0,
'serial_no' : '',
'has_serial_no' : item.has_serial_no,
'has_batch_no' : item.has_batch_no
'has_batch_no' : item.has_batch_no,
'sample_quantity' : item.sample_quantity
})
for d in [["Account", "expense_account", "default_expense_account"],
["Cost Center", "cost_center", "cost_center"]]:
@ -803,6 +805,40 @@ class StockEntry(StockController):
if getdate(self.posting_date) > getdate(expiry_date):
frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code))
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, basestring):
items = json.loads(items)
retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse')
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.company = company
stock_entry.purpose = "Material Transfer"
for item in items:
if item.get('sample_quantity') and item.get('batch_no'):
sample_quantity = validate_sample_quantity(item.get('item_code'), item.get('sample_quantity'), item.get('transfer_qty') or item.get('qty'), item.get('batch_no'))
if sample_quantity:
sample_serial_nos = ''
if item.get('serial_no'):
serial_nos = (item.get('serial_no')).split()
if serial_nos and len(serial_nos) > item.get('sample_quantity'):
serial_no_list = serial_nos[:-(len(serial_nos)-item.get('sample_quantity'))]
sample_serial_nos = '\n'.join(serial_no_list)
stock_entry.append("items", {
"item_code": item.get('item_code'),
"s_warehouse": item.get('t_warehouse'),
"t_warehouse": retention_warehouse,
"qty": item.get('sample_quantity'),
"basic_rate": item.get('valuation_rate'),
'uom': item.get('uom'),
'stock_uom': item.get('stock_uom'),
"conversion_factor": 1.0,
"serial_no": sample_serial_nos,
'batch_no': item.get('batch_no')
})
if stock_entry.get('items'):
return stock_entry.as_dict()
@frappe.whitelist()
def get_production_order_details(production_order):
production_order = frappe.get_doc("Production Order", production_order)
@ -893,5 +929,24 @@ def get_warehouse_details(args):
"actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0,
"basic_rate" : get_incoming_rate(args)
}
return ret
@frappe.whitelist()
def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None):
if cint(qty) < cint(sample_quantity):
frappe.throw(_("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty), alert=True)
retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse')
retainted_qty = 0
if batch_no:
retainted_qty = get_batch_qty(batch_no, retention_warehouse, item_code)
max_retain_qty = frappe.get_value('Item', item_code, 'sample_quantity')
if retainted_qty >= max_retain_qty:
frappe.msgprint(_("Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}.").
format(retainted_qty, batch_no, item_code, batch_no), alert=True)
sample_quantity = 0
qty_diff = max_retain_qty-retainted_qty
if cint(sample_quantity) > cint(qty_diff):
frappe.msgprint(_("Maximum Samples - {0} can be retained for Batch {1} and Item {2}.").
format(max_retain_qty, batch_no, item_code), alert=True)
sample_quantity = qty_diff
return sample_quantity

View File

@ -15,6 +15,7 @@ from erpnext.stock.doctype.item.test_item import set_item_variant_settings, make
from frappe.tests.test_permissions import set_user_permission_doctypes
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse
def get_sle(**args):
condition, values = "", []
@ -613,6 +614,61 @@ class TestStockEntry(unittest.TestCase):
s2.submit()
s2.cancel()
def test_retain_sample(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.batch.batch import get_batch_qty
create_warehouse("Test Warehouse for Sample Retention")
frappe.db.set_value("Stock Settings", None, "sample_retention_warehouse", "Test Warehouse for Sample Retention - _TC")
item = frappe.new_doc("Item")
item.item_code = "Retain Sample Item"
item.item_name = "Retain Sample Item"
item.description = "Retain Sample Item"
item.item_group = "All Item Groups"
item.is_stock_item = 1
item.has_batch_no = 1
item.create_new_batch = 1
item.retain_sample = 1
item.sample_quantity = 4
item.save()
receipt_entry = frappe.new_doc("Stock Entry")
receipt_entry.company = "_Test Company"
receipt_entry.purpose = "Material Receipt"
receipt_entry.append("items", {
"item_code": item.item_code,
"t_warehouse": "_Test Warehouse - _TC",
"qty": 40,
"basic_rate": 12,
"cost_center": "_Test Cost Center - _TC",
"sample_quantity": 4
})
receipt_entry.insert()
receipt_entry.submit()
retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items"))
retention_entry = frappe.new_doc("Stock Entry")
retention_entry.company = retention_data.company
retention_entry.purpose = retention_data.purpose
retention_entry.append("items", {
"item_code": item.item_code,
"t_warehouse": "Test Warehouse for Sample Retention - _TC",
"s_warehouse": "_Test Warehouse - _TC",
"qty": 4,
"basic_rate": 12,
"cost_center": "_Test Cost Center - _TC",
"batch_no": receipt_entry.get("items")[0].batch_no
})
retention_entry.insert()
retention_entry.submit()
qty_in_usable_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item")
qty_in_retention_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "Test Warehouse for Sample Retention - _TC", "_Test Item")
self.assertEquals(qty_in_usable_warehouse, 36)
self.assertEquals(qty_in_retention_warehouse, 4)
def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
se = frappe.copy_doc(test_records[0])
se.get("items")[0].item_code = item_code or "_Test Serialized Item With Series"

View File

@ -140,9 +140,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "clean_description_html",
"fieldtype": "Check",
"fieldname": "sample_retention_warehouse",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -150,9 +149,10 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Convert Item Description to Clean HTML",
"label": "Sample Retention Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -284,6 +284,37 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "clean_description_html",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Convert Item Description to Clean HTML",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -648,7 +679,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-11-14 16:19:50.274518",
"modified": "2017-11-17 01:35:49.562613",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",