Delivery by Serial No (#15030)
* fields added for delivery by Serial No * SO - validate item for delivery by Serial No * Stock Entry - add Serial No on production, validate reserved consumption * add item by reservation to transaction if delivery by Serial No * SLE - validate reserved Serial No by SO in Delivery Note, Sale Invoice * Sales Order - validate Ensure Delivery by Serial No * Serial No - remove SO ref on cancel
This commit is contained in:
parent
7f8024c516
commit
d54991d624
@ -40,6 +40,7 @@ class SalesOrder(SellingController):
|
||||
self.validate_for_items()
|
||||
self.validate_warehouse()
|
||||
self.validate_drop_ship()
|
||||
self.validate_serial_no_based_delivery()
|
||||
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
@ -398,6 +399,32 @@ class SalesOrder(SellingController):
|
||||
d.set("delivery_date", _get_delivery_date(reference_delivery_date,
|
||||
reference_doc.transaction_date, self.transaction_date))
|
||||
|
||||
def validate_serial_no_based_delivery(self):
|
||||
reserved_items = []
|
||||
normal_items = []
|
||||
for item in self.items:
|
||||
if item.ensure_delivery_based_on_produced_serial_no:
|
||||
if item.item_code in normal_items:
|
||||
frappe.throw(_("Cannot ensure delivery by Serial No as \
|
||||
Item {0} is added with and without Ensure Delivery by \
|
||||
Serial No.").format(item.item_code))
|
||||
if item.item_code not in reserved_items:
|
||||
if not frappe.db.get_value("Item", item.item_code, "has_serial_no"):
|
||||
frappe.throw(_("Item {0} has no Serial No. Only serilialized items \
|
||||
can have delivery based on Serial No").format(item.item_code))
|
||||
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
||||
frappe.throw(_("No active BOM found for item {0}. Delivery by \
|
||||
Serial No cannot be ensured").format(item.item_code))
|
||||
reserved_items.append(item.item_code)
|
||||
else:
|
||||
normal_items.append(item.item_code)
|
||||
|
||||
if not item.ensure_delivery_based_on_produced_serial_no and \
|
||||
item.item_code in reserved_items:
|
||||
frappe.throw(_("Cannot ensure delivery by Serial No as \
|
||||
Item {0} is added with and without Ensure Delivery by \
|
||||
Serial No.").format(item.item_code))
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
list_context = get_list_context(context)
|
||||
|
@ -78,6 +78,38 @@
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "ensure_delivery_based_on_produced_serial_no",
|
||||
"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": "Ensure Delivery Based on Produced Serial No",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
@ -2338,7 +2370,7 @@
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2018-05-28 05:52:36.908884",
|
||||
"modified": "2018-07-26 05:52:36.908884",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
@ -2352,4 +2384,4 @@
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
@ -373,6 +373,39 @@
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"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": "Sales Order",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Sales Order",
|
||||
"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,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
@ -1580,7 +1613,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-06-26 15:26:54.476202",
|
||||
"modified": "2018-07-26 15:26:54.476202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
@ -1653,4 +1686,4 @@
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import frappe
|
||||
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate
|
||||
from erpnext.stock.get_item_details import get_reserved_qty_for_so
|
||||
|
||||
from frappe import _, ValidationError
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
@ -241,6 +243,42 @@ def validate_serial_no(sle, item_det):
|
||||
frappe.throw(_("Serial No {0} does not belong to any Warehouse")
|
||||
.format(serial_no), SerialNoWarehouseError)
|
||||
|
||||
# if Sales Order reference in Serial No validate the Delivery Note or Invoice is against the same
|
||||
if sr.sales_order:
|
||||
if sle.voucher_type == "Sales Invoice":
|
||||
if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code, "sales_order": sr.sales_order}):
|
||||
frappe.throw(_("Cannot deliver Serial No {0} of item {1} as it is reserved \
|
||||
to fullfill Sales Order {2}").format(sr.name, sle.item_code, sr.sales_order))
|
||||
elif sle.voucher_type == "Delivery Note":
|
||||
if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code, "against_sales_order": sr.sales_order}):
|
||||
invoice = frappe.db.get_value("Delivery Note Item", {"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code}, "against_sales_invoice")
|
||||
if not invoice or frappe.db.exists("Sales Invoice Item",
|
||||
{"parent": invoice, "item_code": sle.item_code,
|
||||
"sales_order": sr.sales_order}):
|
||||
frappe.throw(_("Cannot deliver Serial No {0} of item {1} as it is reserved to \
|
||||
fullfill Sales Order {2}").format(sr.name, sle.item_code, sr.sales_order))
|
||||
# if Sales Order reference in Delivery Note or Invoice validate SO reservations for item
|
||||
if sle.voucher_type == "Sales Invoice":
|
||||
sales_order = frappe.db.get_value("Sales Invoice Item", {"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code}, "sales_order")
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
elif sle.voucher_type == "Delivery Note":
|
||||
sales_order = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code}, "against_sales_order")
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
else:
|
||||
sales_invoice = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code}, "against_sales_invoice")
|
||||
if sales_invoice:
|
||||
sales_order = frappe.db.get_value("Sales Invoice Item", {
|
||||
"parent": sales_invoice, "item_code": sle.item_code}, "sales_order")
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
elif sle.actual_qty < 0:
|
||||
# transfer out
|
||||
frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
|
||||
@ -248,6 +286,12 @@ def validate_serial_no(sle, item_det):
|
||||
frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
|
||||
SerialNoRequiredError)
|
||||
|
||||
def validate_so_serial_no(sr, sales_order,):
|
||||
if not sr.sales_order or sr.sales_order!= sales_order:
|
||||
frappe.throw(_("""Sales Order {0} has reservation for item {1}, you can
|
||||
only deliver reserved {1} against {0}. Serial No {2} cannot
|
||||
be delivered""").format(sales_order, sr.item_code, sr.name))
|
||||
|
||||
def has_duplicate_serial_no(sn, sle):
|
||||
if sn.warehouse:
|
||||
return True
|
||||
@ -287,7 +331,6 @@ def update_serial_nos(sle, item_det):
|
||||
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
|
||||
frappe.db.set(sle, "serial_no", serial_nos)
|
||||
validate_serial_no(sle, item_det)
|
||||
|
||||
if sle.serial_no:
|
||||
auto_make_serial_nos(sle)
|
||||
|
||||
@ -308,6 +351,8 @@ def auto_make_serial_nos(args):
|
||||
sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None
|
||||
sr.batch_no = args.get('batch_no')
|
||||
sr.location = args.get('location')
|
||||
if sr.sales_order and not args.get('actual_qty', 0) > 0:
|
||||
sr.sales_order = None
|
||||
sr.save(ignore_permissions=True)
|
||||
elif args.get('actual_qty', 0) > 0:
|
||||
make_serial_no(serial_no, args)
|
||||
@ -354,7 +399,7 @@ def update_serial_nos_after_submit(controller, parentfield):
|
||||
if not stock_ledger_entries: return
|
||||
|
||||
for d in controller.get(parentfield):
|
||||
update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice")
|
||||
update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice")
|
||||
and d.rejected_qty) else False
|
||||
accepted_serial_nos_updated = False
|
||||
if controller.doctype == "Stock Entry":
|
||||
@ -402,4 +447,4 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
|
||||
if dn_serial_nos and len(dn_serial_nos)>0:
|
||||
serial_nos = '\n'.join(dn_serial_nos)
|
||||
|
||||
return serial_nos
|
||||
return serial_nos
|
||||
|
@ -8,12 +8,14 @@ from frappe import _
|
||||
from frappe.utils import cstr, cint, flt, comma_or, getdate, nowdate, formatdate, format_time
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError, get_valuation_rate
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor, get_reserved_qty_for_so
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_no, set_batch_nos, get_batch_qty
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, add_additional_cost
|
||||
from erpnext.stock.utils import get_bin
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos
|
||||
|
||||
import json
|
||||
|
||||
from six import string_types, itervalues, iteritems
|
||||
@ -73,7 +75,6 @@ class StockEntry(StockController):
|
||||
|
||||
self.update_stock_ledger()
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
self.update_work_order()
|
||||
self.validate_purchase_order()
|
||||
@ -81,6 +82,10 @@ class StockEntry(StockController):
|
||||
self.update_purchase_order_supplied_items()
|
||||
self.make_gl_entries()
|
||||
self.update_cost_in_project()
|
||||
self.validate_reserved_serial_no_consumption()
|
||||
if self.work_order and self.purpose == "Manufacture":
|
||||
self.update_so_in_serial_number()
|
||||
|
||||
|
||||
def on_cancel(self):
|
||||
|
||||
@ -1046,6 +1051,33 @@ class StockEntry(StockController):
|
||||
stock_bin = get_bin(item_code, reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
|
||||
def update_so_in_serial_number(self):
|
||||
so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"])
|
||||
if so_name and item_code:
|
||||
qty_to_reserve = get_reserved_qty_for_so(so_name, item_code)
|
||||
if qty_to_reserve:
|
||||
reserved_qty = frappe.db.sql("""select count(name) from `tabSerial No` where item_code=%s and
|
||||
sales_order=%s""", (item_code, so_name))
|
||||
if reserved_qty and reserved_qty[0][0]:
|
||||
qty_to_reserve -= reserved_qty[0][0]
|
||||
if qty_to_reserve > 0:
|
||||
for item in self.items:
|
||||
if item.item_code == item_code:
|
||||
serial_nos = (item.serial_no).split("\n")
|
||||
for serial_no in serial_nos:
|
||||
if qty_to_reserve > 0:
|
||||
frappe.db.set_value("Serial No", serial_no, "sales_order", so_name)
|
||||
qty_to_reserve -=1
|
||||
|
||||
def validate_reserved_serial_no_consumption(self):
|
||||
for item in self.items:
|
||||
if item.s_warehouse and not item.t_warehouse and item.serial_no:
|
||||
for sr in get_serial_nos(item.serial_no):
|
||||
sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
|
||||
if sales_order:
|
||||
frappe.throw(_("Item {0} (Serial No: {1}) cannot be consumed as is reserverd\
|
||||
to fullfill Sales Order {2}.").format(item.item_code, sr, sales_order))
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_sample_to_retention_warehouse(company, items):
|
||||
if isinstance(items, string_types):
|
||||
|
@ -91,11 +91,13 @@ def get_item_details(args):
|
||||
out.update(actual_batch_qty)
|
||||
|
||||
if out.has_serial_no and args.get('batch_no'):
|
||||
reserved_so = get_so_reservation_for_item(args)
|
||||
out.batch_no = args.get('batch_no')
|
||||
out.serial_no = get_serial_no(out, args.serial_no)
|
||||
out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
|
||||
|
||||
elif out.has_serial_no:
|
||||
out.serial_no = get_serial_no(out, args.serial_no)
|
||||
reserved_so = get_so_reservation_for_item(args)
|
||||
out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
|
||||
|
||||
if args.transaction_date and item.lead_time_days:
|
||||
out.schedule_date = out.lead_time_date = add_days(args.transaction_date,
|
||||
@ -586,25 +588,32 @@ def get_pos_profile(company, pos_profile=None, user=None):
|
||||
|
||||
return pos_profile and pos_profile[0] or None
|
||||
|
||||
def get_serial_nos_by_fifo(args):
|
||||
def get_serial_nos_by_fifo(args, sales_order=None):
|
||||
if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
|
||||
return "\n".join(frappe.db.sql_list("""select name from `tabSerial No`
|
||||
where item_code=%(item_code)s and warehouse=%(warehouse)s
|
||||
order by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", {
|
||||
where item_code=%(item_code)s and warehouse=%(warehouse)s and
|
||||
sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s)
|
||||
order by timestamp(purchase_date, purchase_time)
|
||||
asc limit %(qty)s""",
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"warehouse": args.warehouse,
|
||||
"qty": abs(cint(args.stock_qty))
|
||||
"qty": abs(cint(args.stock_qty)),
|
||||
"sales_order": sales_order
|
||||
}))
|
||||
|
||||
def get_serial_no_batchwise(args):
|
||||
def get_serial_no_batchwise(args, sales_order=None):
|
||||
if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
|
||||
return "\n".join(frappe.db.sql_list("""select name from `tabSerial No`
|
||||
where item_code=%(item_code)s and warehouse=%(warehouse)s and (batch_no=%(batch_no)s or batch_no is NULL)
|
||||
order by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", {
|
||||
where item_code=%(item_code)s and warehouse=%(warehouse)s and
|
||||
sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s)
|
||||
and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order
|
||||
by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", {
|
||||
"item_code": args.item_code,
|
||||
"warehouse": args.warehouse,
|
||||
"batch_no": args.batch_no,
|
||||
"qty": abs(cint(args.stock_qty))
|
||||
"qty": abs(cint(args.stock_qty)),
|
||||
"sales_order": sales_order
|
||||
}))
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -815,23 +824,21 @@ def get_gross_profit(out):
|
||||
return out
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_serial_no(args, serial_nos=None):
|
||||
def get_serial_no(args, serial_nos=None, sales_order=None):
|
||||
serial_no = None
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
args = frappe._dict(args)
|
||||
|
||||
if args.get('doctype') == 'Sales Invoice' and not args.get('update_stock'):
|
||||
return ""
|
||||
|
||||
if args.get('warehouse') and args.get('stock_qty') and args.get('item_code'):
|
||||
has_serial_no = frappe.get_value('Item', {'item_code': args.item_code}, "has_serial_no")
|
||||
if args.get('batch_no') and has_serial_no == 1:
|
||||
return get_serial_no_batchwise(args)
|
||||
return get_serial_no_batchwise(args, sales_order)
|
||||
elif has_serial_no == 1:
|
||||
args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')})
|
||||
args = process_args(args)
|
||||
serial_no = get_serial_nos_by_fifo(args)
|
||||
serial_no = get_serial_nos_by_fifo(args, sales_order)
|
||||
|
||||
if not serial_no and serial_nos:
|
||||
# For POS
|
||||
@ -871,3 +878,28 @@ def get_blanket_order_details(args):
|
||||
|
||||
blanket_order_details = blanket_order_details[0] if blanket_order_details else ''
|
||||
return blanket_order_details
|
||||
|
||||
def get_so_reservation_for_item(args):
|
||||
reserved_so = None
|
||||
if args.get('against_sales_order'):
|
||||
if get_reserved_qty_for_so(args.get('against_sales_order'), args.get('item_code')):
|
||||
reserved_so = args.get('against_sales_order')
|
||||
elif args.get('against_sales_invoice'):
|
||||
sales_order = frappe.db.sql("""select sales_order from `tabSales Invoice Item` where
|
||||
parent=%s and item_code=%s""", (args.get('against_sales_invoice'), args.get('item_code')))
|
||||
if sales_order and sales_order[0]:
|
||||
if get_reserved_qty_for_so(sales_order[0][0], args.get('item_code')):
|
||||
reserved_so = sales_order[0]
|
||||
elif args.get("sales_order"):
|
||||
if get_reserved_qty_for_so(args.get('sales_order'), args.get('item_code')):
|
||||
reserved_so = args.get('sales_order')
|
||||
return reserved_so
|
||||
|
||||
def get_reserved_qty_for_so(sales_order, item_code):
|
||||
reserved_qty = frappe.db.sql("""select sum(qty) from `tabSales Order Item`
|
||||
where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1
|
||||
""", (sales_order, item_code))
|
||||
if reserved_qty and reserved_qty[0][0]:
|
||||
return reserved_qty[0][0]
|
||||
else:
|
||||
return 0
|
||||
|
Loading…
x
Reference in New Issue
Block a user