def validate_returned_items(doc): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items = frappe._dict() select_fields = "item_code, sum(qty) as qty, rate" if doc.doctype=="Purchase Invoice" \ else "item_code, sum(qty) as qty, rate, serial_no, batch_no" for d in frappe.db.sql("""select {0} from `tab{1} Item` where parent = %s group by item_code""".format(select_fields, doc.doctype), doc.return_against, as_dict=1): valid_items.setdefault(d.item_code, d) if doc.doctype in ("Delivery Note", "Sales Invoice"): for d in frappe.db.sql("""select item_code, sum(qty) as qty, serial_no, batch_no from `tabPacked Item` where parent = %s group by item_code""".format(doc.doctype), doc.return_against, as_dict=1): valid_items.setdefault(d.item_code, d) already_returned_items = get_already_returned_items(doc) # ( not mandatory when it is Purchase Invoice or a Sales Invoice without Update Stock ) warehouse_mandatory = not (doc.doctype=="Purchase Invoice" or (doc.doctype=="Sales Invoice" and not doc.update_stock)) items_returned = False for d in doc.get("items"): if flt(d.qty) < 0: if d.item_code not in valid_items: frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) already_returned_qty = flt(already_returned_items.get(d.item_code)) max_return_qty = flt(ref.qty) - already_returned_qty if already_returned_qty >= ref.qty: frappe.throw(_("Item {0} has already been returned").format(d.item_code), StockOverReturnError) elif abs(d.qty) > max_return_qty: frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") .format(d.idx, ref.qty, d.item_code), StockOverReturnError) elif ref.batch_no and d.batch_no != ref.batch_no: frappe.throw(_("Row # {0}: Batch No must be same as {1} {2}") .format(d.idx, doc.doctype, doc.return_against)) elif ref.serial_no: if not d.serial_no: frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) else: serial_nos = get_serial_nos(d.serial_no) ref_serial_nos = get_serial_nos(ref.serial_no) for s in serial_nos: if s not in ref_serial_nos: frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") .format(d.idx, s, doc.doctype, doc.return_against)) if warehouse_mandatory and not d.get("warehouse"): frappe.throw(_("Warehouse is mandatory")) items_returned = True if not items_returned: frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))
def test_serialized_partial_sales_invoice(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no) serial_no = '\n'.join(serial_no) dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no) si = make_sales_invoice(dn.name) si.items[0].qty = 1 si.submit() self.assertEqual(si.items[0].qty, 1) si = make_sales_invoice(dn.name) si.submit() self.assertEqual(si.items[0].qty, len(get_serial_nos(si.items[0].serial_no)))
def update_rate_in_serial_no(self, purchase_receipt): for item in purchase_receipt.get("purchase_receipt_details"): if item.serial_no: serial_nos = get_serial_nos(item.serial_no) if serial_nos: frappe.db.sql("update `tabSerial No` set purchase_rate=%s where name in ({0})" .format(", ".join(["%s"]*len(serial_nos))), tuple([item.valuation_rate] + serial_nos))
def test_serialize_status(self): from erpnext.stock.doctype.serial_no.serial_no import ( SerialNoStatusError, get_serial_nos, SerialNoDuplicateError, ) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item se = make_serialized_item() serial_nos = get_serial_nos(se.get("items")[0].serial_no) sr = frappe.get_doc("Serial No", serial_nos[0]) sr.status = "Not Available" sr.save() si = frappe.copy_doc(test_records[0]) si.update_stock = 1 si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].qty = 1 si.get("items")[0].serial_no = serial_nos[0] si.insert() self.assertRaises(SerialNoStatusError, si.submit) # hack! because stock ledger entires are already inserted and are not rolled back! self.assertRaises(SerialNoDuplicateError, si.cancel)
def test_return_for_serialized_items(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] dn = create_delivery_note(item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no) self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) # return entry dn1 = create_delivery_note( item_code="_Test Serialized Item With Series", is_return=1, return_against=dn.name, qty=-1, rate=500, serial_no=serial_no, ) self.check_serial_no_values(serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""}) dn1.cancel() self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) dn.cancel() self.check_serial_no_values( serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": "", "purchase_document_no": se.name}, )
def set_missing_item_details(self, for_validate=False): """set missing item values""" from erpnext.stock.get_item_details import get_item_details from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos if hasattr(self, "items"): parent_dict = {} for fieldname in self.meta.get_valid_columns(): parent_dict[fieldname] = self.get(fieldname) if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: document_type = "{} Item".format(self.doctype) parent_dict.update({"document_type": document_type}) for item in self.get("items"): if item.get("item_code"): args = parent_dict.copy() args.update(item.as_dict()) args["doctype"] = self.doctype args["name"] = self.name if not args.get("transaction_date"): args["transaction_date"] = args.get("posting_date") if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted ret = get_item_details(args) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: if (item.get(fieldname) is None or fieldname in force_item_fields): item.set(fieldname, value) elif fieldname in ['cost_center', 'conversion_factor'] and not item.get(fieldname): item.set(fieldname, value) elif fieldname == "serial_no": # Ensure that serial numbers are matched against Stock UOM item_conversion_factor = item.get("conversion_factor") or 1.0 item_qty = abs(item.get("qty")) * item_conversion_factor if item_qty != len(get_serial_nos(item.get('serial_no'))): item.set(fieldname, value) if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) if ret.get("pricing_rule"): # if user changed the discount percentage then set user's discount percentage ? item.set("pricing_rule", ret.get("pricing_rule")) item.set("discount_percentage", ret.get("discount_percentage")) if ret.get("pricing_rule_for") == "Rate": item.set("price_list_rate", ret.get("price_list_rate")) if item.price_list_rate: item.rate = flt(item.price_list_rate * (1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate")) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate)
def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): serial_no = frappe.get_doc("Serial No", serial_no) for field, value in field_values.items(): self.assertEquals(cstr(serial_no.get(field)), value) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] _check_serial_no_values(serial_no, { "warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name }) return_pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=-1, is_return=1, return_against=pr.name, serial_no=serial_no) _check_serial_no_values(serial_no, { "warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name })
def get_ref_item_dict(valid_items, ref_item_row): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items.setdefault( ref_item_row.item_code, frappe._dict({ "qty": 0, "rejected_qty": 0, "received_qty": 0, "serial_no": [], "batch_no": [] })) item_dict = valid_items[ref_item_row.item_code] item_dict["qty"] += ref_item_row.qty if ref_item_row.parenttype in ['Purchase Invoice', 'Purchase Receipt']: item_dict["received_qty"] += ref_item_row.received_qty item_dict["rejected_qty"] += ref_item_row.rejected_qty if ref_item_row.get("serial_no"): item_dict["serial_no"] += get_serial_nos(ref_item_row.serial_no) if ref_item_row.get("batch_no"): item_dict["batch_no"].append(ref_item_row.batch_no) return valid_items
def test_serialized(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos se = make_serialized_item() serial_nos = get_serial_nos(se.get("items")[0].serial_no) si = frappe.copy_doc(test_records[0]) si.update_stock = 1 si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].qty = 1 si.get("items")[0].serial_no = serial_nos[0] si.insert() si.submit() self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"), si.name) # check if the serial number is already linked with any other Sales Invoice _si = frappe.copy_doc(si.as_dict()) self.assertRaises(frappe.ValidationError, _si.insert) return si
def create_asset_movement(**args): args = frappe._dict(args) if not args.transaction_date: args.transaction_date = now() movement = frappe.new_doc("Asset Movement") movement.update({ "asset": args.asset, "transaction_date": args.transaction_date, "target_location": args.target_location, "company": args.company, 'purpose': args.purpose or 'Receipt', 'serial_no': args.serial_no, 'quantity': len(get_serial_nos(args.serial_no)) if args.serial_no else 1, 'from_employee': "_T-Employee-00001" or args.from_employee, 'to_employee': args.to_employee }) if args.source_location: movement.update({ 'source_location': args.source_location }) movement.insert() movement.submit() return movement
def test_serialize_status(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] frappe.db.set_value("Serial No", serial_no, "status", "Not Available") dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no, do_not_submit=True) self.assertRaises(SerialNoStatusError, dn.submit)
def test_serialized_cancel(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos si = self.test_serialized() si.cancel() serial_nos = get_serial_nos(si.get("items")[0].serial_no) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC") self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"))
def validate_returned_items(doc): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items = frappe._dict() select_fields = "item_code, qty, parenttype" if doc.doctype=="Purchase Invoice" \ else "item_code, qty, serial_no, batch_no, parenttype" if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql("""select {0} from `tab{1} Item` where parent = %s""" .format(select_fields, doc.doctype), doc.return_against, as_dict=1): valid_items = get_ref_item_dict(valid_items, d) if doc.doctype in ("Delivery Note", "Sales Invoice"): for d in frappe.db.sql("""select item_code, qty, serial_no, batch_no from `tabPacked Item` where parent = %s""".format(doc.doctype), doc.return_against, as_dict=1): valid_items = get_ref_item_dict(valid_items, d) already_returned_items = get_already_returned_items(doc) # ( not mandatory when it is Purchase Invoice or a Sales Invoice without Update Stock ) warehouse_mandatory = not (doc.doctype=="Purchase Invoice" or (doc.doctype=="Sales Invoice" and not doc.update_stock)) items_returned = False for d in doc.get("items"): if flt(d.qty) < 0 or d.get('received_qty') < 0: if d.item_code not in valid_items: frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) validate_quantity(doc, d, ref, valid_items, already_returned_items) if ref.batch_no and d.batch_no not in ref.batch_no: frappe.throw(_("Row # {0}: Batch No must be same as {1} {2}") .format(d.idx, doc.doctype, doc.return_against)) elif ref.serial_no: if not d.serial_no: frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) else: serial_nos = get_serial_nos(d.serial_no) for s in serial_nos: if s not in ref.serial_no: frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") .format(d.idx, s, doc.doctype, doc.return_against)) if warehouse_mandatory and not d.get("warehouse"): frappe.throw(_("Warehouse is mandatory")) items_returned = True if not items_returned: frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))
def test_serialized_cancel(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos dn = self.test_serialized() dn.cancel() serial_nos = get_serial_nos(dn.get("delivery_note_details")[0].serial_no) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Available") self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC") self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"))
def test_serialized(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) dn.cancel() self.check_serial_no_values(serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""})
def validate_serial_against_delivery_note(self): """ validate if the serial numbers in Sales Invoice Items are same as in Delivery Note Item """ for item in self.items: if not item.delivery_note or not item.dn_detail: continue serial_nos = frappe.db.get_value("Delivery Note Item", item.dn_detail, "serial_no") or "" dn_serial_nos = set(get_serial_nos(serial_nos)) serial_nos = item.serial_no or "" si_serial_nos = set(get_serial_nos(serial_nos)) if si_serial_nos - dn_serial_nos: frappe.throw(_("Serial Numbers in row {0} does not match with Delivery Note".format(item.idx))) if item.serial_no and cint(item.qty) != len(si_serial_nos): frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.".format( item.idx, item.qty, item.item_code, len(si_serial_nos))))
def test_inter_company_transfer(self): se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]) wh = create_warehouse("_Test Warehouse", company="_Test Company 1") make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) serial_no = frappe.db.get_value("Serial No", serial_nos[0], ["warehouse", "company"], as_dict=1) self.assertEqual(serial_no.warehouse, wh) self.assertEqual(serial_no.company, "_Test Company 1")
def make_asset_movement(self, row): asset_movement = frappe.get_doc({ 'doctype': 'Asset Movement', 'asset': row.asset, 'target_location': row.asset_location, 'purpose': 'Receipt', 'serial_no': row.serial_no, 'quantity': len(get_serial_nos(row.serial_no)), 'company': self.company, 'transaction_date': self.posting_date, 'reference_doctype': self.doctype, 'reference_name': self.name }).insert() return asset_movement.name
def validate_asset(self): status, company = frappe.db.get_value("Asset", self.asset, ["status", "company"]) if self.purpose == 'Transfer' and status in ("Draft", "Scrapped", "Sold"): frappe.throw(_("{0} asset cannot be transferred").format(status)) if company != self.company: frappe.throw(_("Asset {0} does not belong to company {1}").format(self.asset, self.company)) if self.serial_no and len(get_serial_nos(self.serial_no)) != self.quantity: frappe.throw(_("Number of serial nos and quantity must be the same")) if not(self.source_location or self.target_location or self.from_employee or self.to_employee): frappe.throw(_("Either location or employee must be required")) if (not self.serial_no and frappe.db.get_value('Serial No', {'asset': self.asset}, 'name')): frappe.throw(_("Serial no is required for the asset {0}").format(self.asset))
def test_serialize_status(self): from erpnext.stock.doctype.serial_no.serial_no import SerialNoStatusError, get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item se = make_serialized_item() serial_nos = get_serial_nos(se.get("mtn_details")[0].serial_no) sr = frappe.get_doc("Serial No", serial_nos[0]) sr.status = "Not Available" sr.save() dn = frappe.copy_doc(test_records[0]) dn.get("delivery_note_details")[0].item_code = "_Test Serialized Item With Series" dn.get("delivery_note_details")[0].qty = 1 dn.get("delivery_note_details")[0].serial_no = serial_nos[0] dn.insert() self.assertRaises(SerialNoStatusError, dn.submit)
def get_ref_item_dict(valid_items, ref_item_row): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items.setdefault(ref_item_row.item_code, frappe._dict({ "qty": 0, "serial_no": [], "batch_no": [] })) item_dict = valid_items[ref_item_row.item_code] item_dict["qty"] += ref_item_row.qty if ref_item_row.get("serial_no"): item_dict["serial_no"] += get_serial_nos(ref_item_row.serial_no) if ref_item_row.get("batch_no"): item_dict["batch_no"].append(ref_item_row.batch_no) return valid_items
def set_latest_location_in_asset(self): location, employee = '', '' cond = "1=1" args = { 'asset': self.asset, 'company': self.company } if self.serial_no: cond = "serial_no like %(txt)s" args.update({ 'txt': "%%%s%%" % self.serial_no }) latest_movement_entry = frappe.db.sql("""select target_location, to_employee from `tabAsset Movement` where asset=%(asset)s and docstatus=1 and company=%(company)s and {0} order by transaction_date desc limit 1""".format(cond), args) if latest_movement_entry: location = latest_movement_entry[0][0] employee = latest_movement_entry[0][1] elif self.purpose in ['Transfer', 'Receipt']: movement_entry = frappe.db.sql("""select source_location, from_employee from `tabAsset Movement` where asset=%(asset)s and docstatus=2 and company=%(company)s and {0} order by transaction_date asc limit 1""".format(cond), args) if movement_entry: location = movement_entry[0][0] employee = movement_entry[0][1] if not self.serial_no: frappe.db.set_value("Asset", self.asset, "location", location) if not employee and self.purpose in ['Receipt', 'Transfer']: employee = self.to_employee if self.serial_no: for d in get_serial_nos(self.serial_no): if (location or (self.purpose == 'Issue' and self.source_location)): frappe.db.set_value('Serial No', d, 'location', location) if employee or self.docstatus==2 or self.purpose == 'Issue': frappe.db.set_value('Serial No', d, 'employee', employee)
def test_serialize_status(self): from erpnext.stock.doctype.serial_no.serial_no import SerialNoStatusError, get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item se = make_serialized_item() serial_nos = get_serial_nos(se.get("items")[0].serial_no) sr = frappe.get_doc("Serial No", serial_nos[0]) sr.status = "Not Available" sr.save() si = frappe.copy_doc(test_records[0]) si.update_stock = 1 si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].qty = 1 si.get("items")[0].serial_no = serial_nos[0] si.insert() self.assertRaises(SerialNoStatusError, si.submit)
def test_serialized(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos se = make_serialized_item() serial_nos = get_serial_nos(se.get("mtn_details")[0].serial_no) dn = frappe.copy_doc(test_records[0]) dn.get("delivery_note_details")[0].item_code = "_Test Serialized Item With Series" dn.get("delivery_note_details")[0].qty = 1 dn.get("delivery_note_details")[0].serial_no = serial_nos[0] dn.insert() dn.submit() self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Delivered") self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), dn.name) return dn
def test_serial_numbers_against_delivery_note(self): """ check if the sales invoice item serial numbers and the delivery note items serial numbers are same """ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos se = make_serialized_item() serial_nos = get_serial_nos(se.get("items")[0].serial_no) dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=serial_nos[0]) dn.submit() si = make_sales_invoice(dn.name) si.save() self.assertEquals(si.get("items")[0].serial_no, dn.get("items")[0].serial_no)
def test_serialized(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos se = make_serialized_item() serial_nos = get_serial_nos(se.get("items")[0].serial_no) si = frappe.copy_doc(test_records[0]) si.update_stock = 1 si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].qty = 1 si.get("items")[0].serial_no = serial_nos[0] si.insert() si.submit() self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) self.assertEquals(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name) return si
def test_serialized(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) self.check_serial_no_values(serial_no, { "warehouse": "", "delivery_document_no": dn.name }) si = make_sales_invoice(dn.name) si.insert(ignore_permissions=True) self.assertEqual(dn.items[0].serial_no, si.items[0].serial_no) dn.cancel() self.check_serial_no_values(serial_no, { "warehouse": "_Test Warehouse - _TC", "delivery_document_no": "" })
def validate_location(self): if self.purpose in ['Transfer', 'Issue']: if not self.serial_no and not (self.from_employee or self.to_employee): self.source_location = frappe.db.get_value("Asset", self.asset, "location") if self.purpose == 'Issue' and not (self.source_location or self.from_employee): frappe.throw(_("Source Location is required for the asset {0}").format(self.asset)) if self.serial_no and self.source_location: s_nos = get_serial_nos(self.serial_no) serial_nos = frappe.db.sql_list(""" select name from `tabSerial No` where location != '%s' and name in (%s)""" %(self.source_location, ','.join(['%s'] * len(s_nos))), tuple(s_nos)) if serial_nos: frappe.throw(_("Serial nos {0} does not belongs to the location {1}"). format(','.join(serial_nos), self.source_location)) if self.source_location and self.source_location == self.target_location and self.purpose == 'Transfer': frappe.throw(_("Source and Target Location cannot be same")) if self.purpose == 'Receipt' and not (self.target_location or self.to_employee): frappe.throw(_("Target Location is required for the asset {0}").format(self.asset))
def validate_returned_items(doc): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items = frappe._dict() select_fields = "item_code, sum(qty) as qty, rate" if doc.doctype=="Purchase Invoice" \ else "item_code, sum(qty) as qty, rate, serial_no, batch_no" for d in frappe.db.sql("""select {0} from `tab{1} Item` where parent = %s group by item_code""".format(select_fields, doc.doctype), doc.return_against, as_dict=1): valid_items.setdefault(d.item_code, d) if doc.doctype in ("Delivery Note", "Sales Invoice"): for d in frappe.db.sql( """select item_code, sum(qty) as qty, serial_no, batch_no from `tabPacked Item` where parent = %s group by item_code""".format(doc.doctype), doc.return_against, as_dict=1): valid_items.setdefault(d.item_code, d) already_returned_items = get_already_returned_items(doc) items_returned = False for d in doc.get("items"): if flt(d.qty) < 0: if d.item_code not in valid_items: frappe.throw( _("Row # {0}: Returned Item {1} does not exists in {2} {3}" ).format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) already_returned_qty = flt( already_returned_items.get(d.item_code)) max_return_qty = flt(ref.qty) - already_returned_qty if already_returned_qty >= ref.qty: frappe.throw( _("Item {0} has already been returned").format( d.item_code), StockOverReturnError) elif abs(d.qty) > max_return_qty: frappe.throw( _("Row # {0}: Cannot return more than {1} for Item {2}" ).format(d.idx, ref.qty, d.item_code), StockOverReturnError) elif ref.batch_no and d.batch_no != ref.batch_no: frappe.throw( _("Row # {0}: Batch No must be same as {1} {2}"). format(d.idx, doc.doctype, doc.return_against)) elif ref.serial_no: if not d.serial_no: frappe.throw( _("Row # {0}: Serial No is mandatory").format( d.idx)) else: serial_nos = get_serial_nos(d.serial_no) ref_serial_nos = get_serial_nos(ref.serial_no) for s in serial_nos: if s not in ref_serial_nos: frappe.throw( _("Row # {0}: Serial No {1} does not match with {2} {3}" ).format(d.idx, s, doc.doctype, doc.return_against)) items_returned = True if not items_returned: frappe.throw( _("Atleast one item should be entered with negative quantity in return document" ))
def get_serial_nos_data(serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos return get_serial_nos(serial_nos)
def validate_returned_items(doc): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos valid_items = frappe._dict() select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor" if doc.doctype != 'Purchase Invoice': select_fields += ",serial_no, batch_no" if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql("""select {0} from `tab{1} Item` where parent = %s""" .format(select_fields, doc.doctype), doc.return_against, as_dict=1): valid_items = get_ref_item_dict(valid_items, d) if doc.doctype in ("Delivery Note", "Sales Invoice"): for d in frappe.db.sql("""select item_code, qty, serial_no, batch_no from `tabPacked Item` where parent = %s""".format(doc.doctype), doc.return_against, as_dict=1): valid_items = get_ref_item_dict(valid_items, d) already_returned_items = get_already_returned_items(doc) # ( not mandatory when it is Purchase Invoice or a Sales Invoice without Update Stock ) warehouse_mandatory = not ((doc.doctype=="Purchase Invoice" or doc.doctype=="Sales Invoice") and not doc.update_stock) items_returned = False for d in doc.get("items"): if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0): if d.item_code not in valid_items: frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) validate_quantity(doc, d, ref, valid_items, already_returned_items) if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate: frappe.throw(_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}") .format(d.idx, doc.doctype, doc.return_against)) elif ref.batch_no and d.batch_no not in ref.batch_no: frappe.throw(_("Row # {0}: Batch No must be same as {1} {2}") .format(d.idx, doc.doctype, doc.return_against)) elif ref.serial_no: if not d.serial_no: frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) else: serial_nos = get_serial_nos(d.serial_no) for s in serial_nos: if s not in ref.serial_no: frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") .format(d.idx, s, doc.doctype, doc.return_against)) if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \ and not d.get("warehouse"): frappe.throw(_("Warehouse is mandatory")) items_returned = True elif d.item_name: items_returned = True if not items_returned: frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))
def test_stock_reco_for_serialized_item(self): set_perpetual_inventory() to_delete_records = [] to_delete_serial_nos = [] # Add new serial nos serial_item_code = "Stock-Reco-Serial-Item-1" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" sr = create_stock_reconciliation(item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200) # print(sr.name) serial_nos = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos), 5) args = { "item_code": serial_item_code, "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), "serial_no": sr.items[0].serial_no } valuation_rate = get_incoming_rate(args) self.assertEqual(valuation_rate, 200) to_delete_records.append(sr.name) sr = create_stock_reconciliation(item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300, serial_no='\n'.join(serial_nos)) # print(sr.name) serial_nos1 = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos1), 5) args = { "item_code": serial_item_code, "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), "serial_no": sr.items[0].serial_no } valuation_rate = get_incoming_rate(args) self.assertEqual(valuation_rate, 300) to_delete_records.append(sr.name) to_delete_records.reverse() for d in to_delete_records: stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() frappe.delete_doc("Stock Reconciliation", stock_doc.name) for d in serial_nos + serial_nos1: if frappe.db.exists("Serial No", d): frappe.delete_doc("Serial No", d)
def update_serial(source, target, parent): serial_nos = get_serial_nos(target.serial_no) if len(serial_nos) == 1: target.serial_no = serial_nos[0] else: target.serial_no = ""
def get_sle_for_serialized_items(self, row, sl_entries): from erpnext.stock.stock_ledger import get_previous_sle serial_nos = get_serial_nos(row.serial_no) # To issue existing serial nos if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) args.update({ 'actual_qty': -1 * row.current_qty, 'serial_no': row.current_serial_no, 'batch_no': row.batch_no, 'valuation_rate': row.current_valuation_rate }) if row.current_serial_no: args.update({ 'qty_after_transaction': 0, }) sl_entries.append(args) qty_after_transaction = 0 for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) previous_sle = get_previous_sle({ "item_code": row.item_code, "posting_date": self.posting_date, "posting_time": self.posting_time, "serial_no": serial_no }) if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse warehouse = previous_sle.get("warehouse", '') or row.warehouse if not qty_after_transaction: qty_after_transaction = get_stock_balance( row.item_code, warehouse, self.posting_date, self.posting_time) qty_after_transaction -= 1 new_args = args.copy() new_args.update({ 'actual_qty': -1, 'qty_after_transaction': qty_after_transaction, 'warehouse': warehouse, 'valuation_rate': previous_sle.get("valuation_rate") }) sl_entries.append(new_args) if row.qty: args = self.get_sle_for_items(row) args.update({ 'actual_qty': row.qty, 'incoming_rate': row.valuation_rate, 'valuation_rate': row.valuation_rate }) sl_entries.append(args) if serial_nos == get_serial_nos(row.current_serial_no): # update valuation rate self.update_valuation_rate_for_serial_nos(row, serial_nos)
def update_valuation_rate_for_serial_no(self): for d in self.items: if not d.serial_no: continue serial_nos = get_serial_nos(d.serial_no) self.update_valuation_rate_for_serial_nos(d, serial_nos)
def test_return_non_consumed_materials(self): """ - Set backflush based on Material Transfer - Create subcontracted PO for the item Subcontracted Item SA2. - Transfer the components from Stores to Supplier warehouse with serial nos. - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. - Create purchase receipt for full qty against the PO and change the qty of raw material. - After that return the non consumed material back to the store from supplier's warehouse. """ set_backflush_based_on("Material Transferred for Subcontract") items = [{ "warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA2", "qty": 5, "rate": 100, }] rm_items = [{ "item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2" }] itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") for d in rm_items: d["po_detail"] = po.items[0].name make_stock_transfer_entry( po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) pr1 = make_purchase_receipt(po.name) pr1.save() pr1.supplied_items[0].consumed_qty = 5 pr1.supplied_items[0].serial_no = "\n".join( sorted( itemwise_details.get("Subcontracted SRM Item 2").get( "serial_no")[0:5])) pr1.submit() for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 5) self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5])) po.load_from_db() self.assertEqual(po.supplied_items[0].consumed_qty, 5) doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items]) self.assertEqual(doc.items[0].qty, 1) self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC") self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC") self.assertEqual( get_serial_nos(doc.items[0].serial_no), itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6], )
def set_missing_item_details(self, for_validate=False): """set missing item values""" from erpnext.stock.get_item_details import get_item_details from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos if hasattr(self, "items"): parent_dict = {} for fieldname in self.meta.get_valid_columns(): parent_dict[fieldname] = self.get(fieldname) if self.doctype in [ "Quotation", "Sales Order", "Delivery Note", "Sales Invoice" ]: document_type = "{} Item".format(self.doctype) parent_dict.update({"document_type": document_type}) for item in self.get("items"): if item.get("item_code"): args = parent_dict.copy() args.update(item.as_dict()) args["doctype"] = self.doctype args["name"] = self.name if not args.get("transaction_date"): args["transaction_date"] = args.get("posting_date") if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted ret = get_item_details(args) for fieldname, value in ret.items(): if item.meta.get_field( fieldname) and value is not None: if (item.get(fieldname) is None or fieldname in force_item_fields): item.set(fieldname, value) elif fieldname in [ 'cost_center', 'conversion_factor' ] and not item.get(fieldname): item.set(fieldname, value) elif fieldname == "serial_no": stock_qty = item.get( "stock_qty") * -1 if item.get( "stock_qty") < 0 else item.get( "stock_qty") if stock_qty != len( get_serial_nos(item.get('serial_no'))): item.set(fieldname, value) if ret.get("pricing_rule"): # if user changed the discount percentage then set user's discount percentage ? item.set("discount_percentage", ret.get("discount_percentage")) if ret.get("pricing_rule_for") == "Price": item.set("pricing_list_rate", ret.get("pricing_list_rate")) if item.price_list_rate: item.rate = flt( item.price_list_rate * (1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate")) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate)
def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1 * source_doc.qty if source_doc.serial_no: returned_serial_nos = get_returned_serial_nos( source_doc, source_parent) serial_nos = list( set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos)) if serial_nos: target_doc.serial_no = '\n'.join(serial_nos) if doctype == "Purchase Receipt": returned_qty_map = get_returned_qty_map_for_row( source_doc.name, doctype) target_doc.received_qty = -1 * flt(source_doc.received_qty - ( returned_qty_map.get('received_qty') or 0)) target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - ( returned_qty_map.get('rejected_qty') or 0)) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - ( returned_qty_map.get('stock_qty') or 0)) target_doc.received_stock_qty = -1 * flt( source_doc.received_stock_qty - (returned_qty_map.get('received_stock_qty') or 0)) target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": returned_qty_map = get_returned_qty_map_for_row( source_doc.name, doctype) target_doc.received_qty = -1 * flt(source_doc.received_qty - ( returned_qty_map.get('received_qty') or 0)) target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - ( returned_qty_map.get('rejected_qty') or 0)) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - ( returned_qty_map.get('stock_qty') or 0)) target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_receipt = source_doc.purchase_receipt target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name target_doc.price_list_rate = 0 elif doctype == "Delivery Note": returned_qty_map = get_returned_qty_map_for_row( source_doc.name, doctype) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - ( returned_qty_map.get('stock_qty') or 0)) target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_invoice = source_doc.against_sales_invoice target_doc.so_detail = source_doc.so_detail target_doc.si_detail = source_doc.si_detail target_doc.expense_account = source_doc.expense_account target_doc.dn_detail = source_doc.name if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": returned_qty_map = get_returned_qty_map_for_row( source_doc.name, doctype) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - ( returned_qty_map.get('stock_qty') or 0)) target_doc.sales_order = source_doc.sales_order target_doc.delivery_note = source_doc.delivery_note target_doc.so_detail = source_doc.so_detail target_doc.dn_detail = source_doc.dn_detail target_doc.expense_account = source_doc.expense_account if doctype == "Sales Invoice": target_doc.sales_invoice_item = source_doc.name else: target_doc.pos_invoice_item = source_doc.name target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return
def set_serial_no_against_delivery_note(self): for item in self.items: if item.serial_no and item.delivery_note and \ item.qty != len(get_serial_nos(item.serial_no)): item.serial_no = get_delivery_note_serial_no(item.item_code, item.qty, item.delivery_note)
def set_serial_no_against_delivery_note(self): for item in self.items: if item.serial_no and item.delivery_note and \ item.qty != len(get_serial_nos(item.serial_no)): item.serial_no = get_delivery_note_serial_no( item.item_code, item.qty, item.delivery_note)
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): """ Behaviour: 1) Create Stock Reconciliation, which will be the origin document of a new batch having a serial no 2) Create a Stock Entry that adds a serial no to the same batch following this Stock Reconciliation 3) Cancel Stock Reconciliation 4) Cancel Stock Entry Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since Stock Entry is dependent on the batch involved 4) Serial No only in the Stock Entry is Inactive and Batch qty decreases """ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.batch.batch import get_batch_qty set_perpetual_inventory() item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item 1'}) if not item: item = create_item("Batched and Serialised Item 1") item.has_batch_no = 1 item.create_new_batch = 1 item.has_serial_no = 1 item.batch_number_series = "B-BATCH-.##" item.serial_no_series = "S-.####" item.save() else: item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item 1'}) warehouse = "_Test Warehouse for Stock Reco2 - _TC" stock_reco = create_stock_reconciliation(item_code=item.item_code, warehouse = warehouse, qty=1, rate=100) batch_no = stock_reco.items[0].batch_no serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] stock_entry = make_stock_entry(item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no) serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0] # Check Batch qty after 2 transactions batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) self.assertEqual(batch_qty, 2) frappe.db.commit() # Cancelling Origin Document of Batch self.assertRaises(frappe.LinkExistsError, stock_reco.cancel) frappe.db.rollback() stock_entry.cancel() # Check Batch qty after cancellation batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) self.assertEqual(batch_qty, 1) # Check if Serial No from Stock Reconcilation is intact self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no) self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") # Check if Serial No from Stock Entry is Unlinked and Inactive self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None) self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive") stock_reco.load_from_db() stock_reco.cancel() for sn in (serial_no, serial_no_2): if frappe.db.exists("Serial No", sn): frappe.delete_doc("Serial No", sn)