[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

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",