most time getting modifictaion error when i saving or submit doc, please let me know why comes this type of error and also let me provide best suggestion for resolving this error permanantly…
fpr more references i am attaching screenshot beow ….
most time getting modifictaion error when i saving or submit doc, please let me know why comes this type of error and also let me provide best suggestion for resolving this error permanantly…
fpr more references i am attaching screenshot beow ….
You have some custom logic hooked that is causing a race condition. Share your doctype controller/server scripts or any method directly or indirectly affecting this doctype.
can you please let me know solution of this
Please share so i can further assist
this is my server side custom_script..
hooks.py
doc_events{
“Stock Entry”: {
"before_insert": "custom_apaulsoft.custom_script.stock_entry.stock_entry.before_insert",
“before_submit”: “custom_apaulsoft.custom_script.stock_entry.stock_entry.before_submit”,
“validate”: “custom_apaulsoft.custom_script.stock_entry.stock_entry.validate”,
“on_update”: “custom_apaulsoft.custom_script.stock_entry.stock_entry.handle_transition”,
# “on_trash”: “custom_apaulsoft.custom_script.stock_entry.stock_entry.clear_tracker”,
},
}
doctype_js{
"Stock Entry": "custom_script/stock_entry/stock_entry.js",
}
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils import cint
from erpnext.stock.doctype.batch.batch import get_batch_qty as _get_batch_qty
from custom_apaulsoft import revised_todo_utils
from custom_apaulsoft.message_templates import show_error_message
from .stock_entry_helper.fatch_uid_from_incoming_iqc import fatch_uid_from_incoming_iqc
from custom_apaulsoft.custom_script.serial_no.serial_no import is_bypassing_user
def before_submit(doc, method=None):
work_order = doc.work_order
if not work_order:
return None
if is_bypassing_user():
return None
serial_numbers = frappe.get_all(
“Serial No”, filters={“work_order”: work_order}, pluck=“name”
)
# for num in serial_numbers:
# if not check_for_pqc_record(docname=num):
# show_error_message(
# message=f"PQC has not been done for Serial Number {num}, due to which this Stock Entry cannot be submitted. Please first perform the PQC for this serial number and then try submitting the Stock Entry",
# translate=True,
# )
# return None
def validate(doc, method=None):
current_workflow_state = frappe.db.get_value(
doc.doctype, doc.name, “workflow_state”
)
doc.custom_previous_workflow_state = current_workflow_state
def handle_transition(doc, method=None):
previous_workflow_state = doc.custom_previous_workflow_state
current_workflow_state = doc.workflow_state
if (
previous_workflow_state is None
and current_workflow_state == “Pending for Review”
) or (
previous_workflow_state == “Open”
and current_workflow_state == “Pending for Review”
):
create_todo_for_purpose(stock_entry=doc)
if previous_workflow_state == current_workflow_state:
return
if previous_workflow_state == “Pending for Review”:
if current_workflow_state in [“Pending for Manager Approval”, “Reviewed”]:
close_todo(
stock_entry=doc,
custom_corresponding_state_name=previous_workflow_state,
)
create_todo_for_purpose(stock_entry=doc)
elif current_workflow_state in [“Rejected by Reviewer”, “Rejected”]:
close_todo_with_rejection(
stock_entry=doc,
rejected_from_state=“Pending for Review”,
rejected_to_state=current_workflow_state,
)
elif previous_workflow_state in [“Pending for Manager Approval”, “Reviewed”]:
if current_workflow_state == “Approved”:
close_todo(
stock_entry=doc,
custom_corresponding_state_name=previous_workflow_state,
)
doc.custom_previous_workflow_state = None
return
elif current_workflow_state == “Rejected by Approver”:
close_todo_with_rejection(
stock_entry=doc,
rejected_from_state=previous_workflow_state,
rejected_to_state=current_workflow_state,
)
close_all_related_todos(stock_entry=doc)
elif previous_workflow_state == “Approved”:
if current_workflow_state == “Dispatched”:
if not check_shipment_logging_status(
stock_entry=doc.name, site_mr_reference=doc.custom_for_site_mr_number
):
show_error_message(
message=f"You cannot transition to Dispatched before filling out the Shipment Details. Please click on Create > Shipment to log the shipment details"
)
return None
else:
doc.custom_dispatch_declaration_acknowledged = 1
elif previous_workflow_state == “Rejected by Reviewer”:
if current_workflow_state == “Pending for Review”:
create_todo_for_purpose(stock_entry=doc)
frappe.db.set_value(
doc.doctype, doc.name, “custom_previous_workflow_state”, current_workflow_state
)
def close_todo_with_rejection(stock_entry, rejected_from_state, rejected_to_state):
return revised_todo_utils.close_todo_with_rejection(
reference_document=stock_entry,
rejected_from_state=rejected_from_state,
rejected_to_state=rejected_to_state,
)
def close_all_related_todos(stock_entry):
return revised_todo_utils.close_all_related_todos(reference_document=stock_entry)
def create_todo_for_purpose(stock_entry):
return revised_todo_utils.create_todo_for_purpose(reference_document=stock_entry)
def close_todo(stock_entry, custom_corresponding_state_name):
return revised_todo_utils.close_todo(
reference_document=stock_entry,
custom_corresponding_state_name=custom_corresponding_state_name,
)
# ---------------------------------------------------------------------#
# This code is for the implementation of the Dispactched feature and other aligning features
def check_shipment_logging_status(stock_entry=None, site_mr_reference=None):
if not stock_entry or not site_mr_reference:
return False
documents = frappe.get_all(
“Shipment Stock Entry”,
filters={“stock_entry”: stock_entry, “site_mr_reference”: site_mr_reference},
fields=[“parent”],
)
for doc in documents:
name = doc[“parent”]
shipment = frappe.get_doc(“Shipment”, name)
if shipment.docstatus != 1:
return False
return True
@frappe.whitelist()
def acknowledge_dispatch_received(docname, field_return_number):
user = frappe.session.user
try:
doc = frappe.get_doc(“Stock Entry”, docname)
doc.add_comment(
“Comment”,
text=(
f"{user} acknowledged the receipt of items against the Field Return Entry number {field_return_number}"
f"at {frappe.utils.now_datetime()} on {frappe.utils.nowdate()}"
),
)
doc.workflow_state = “Incoming Dispatch Received”
doc.save()
return True
except Exception as e:
frappe.throw(
f"An error occured while adding acknowledgement comment : {str(e)}"
)
‘’’
Automatic quntity fatched when select manually batch in “Update Batch Nos” dialog
‘’’
@frappe.whitelist()
def get_batch_available_qty(batch_no, warehouse, posting_date=None, posting_time=None):
“”“Return available qty for a batch in a warehouse (as-of posting date/time if given).”“”
if not batch_no or not warehouse:
return 0.0
item_code = frappe.db.get_value(“Batch”, batch_no, “item”)
qty = _get_batch_qty(
batch_no=batch_no,
warehouse=warehouse,
item_code=item_code,
posting_date=posting_date,
posting_time=posting_time,
)
return flt(qty or 0)
@frappe.whitelist()
def get_all_batches_for_item(item_code, warehouse, required_qty=None, stock_entry_name=None):
“”“Return limited batches up to required_qty, matching item table qty (not dialog Qty to Fetch).”“”
from frappe import _dict
from frappe.utils import flt
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import \
get_auto_batch_nos
try:
required_qty = flt(required_qty or 0)
if required_qty <= 0:
return {“total_qty”: 0, “batches”: }
kwargs = _dict({
“item_code”: item_code,
“warehouse”: warehouse,
“has_batch_no”: 1,
“consider_negative_batches”: False,
“based_on”: “FIFO”,
})
batches = get_auto_batch_nos(kwargs)
# batches.sort(key=lambda b: flt(b.qty))
import re
def natural_key(text):
return [int(t) if t.isdigit() else t for t in re.split(r’(\d+)', text)]
batches.sort(key=lambda b: natural_key(b.get(“batch_no”)))
selected_batches =
total_picked = 0.0
cache = frappe.cache()
cache_key = f"batch_usage_{stock_entry_name}_{item_code}"
cache.delete_value(cache_key)
usage_cache = {}
usage_cache = cache.get_value(cache_key) or {}
if len(batches) == 1:
b = batches[0]
batch_qty = flt(b.qty)
pick_qty = required_qty if batch_qty > required_qty else batch_qty
selected_batches.append({
“batch_no”: b.get(“batch_no”),
“picked_qty”: flt(pick_qty, 3),
“available_qty”: batch_qty,
})
return {
“total_qty”: flt(pick_qty, 3),
“batches”: selected_batches,
}
# for b in batches:
# if total_picked >= required_qty:
# break
# batch_qty = flt(b.qty)
# remaining = required_qty - total_picked
# pick_qty = batch_qty if batch_qty <= remaining else remaining
# selected_batches.append({
# “batch_no”: b.get(“batch_no”),
# “picked_qty”: flt(pick_qty, 3),
# “available_qty”: batch_qty,
# })
# total_picked += pick_qty
# print(“batchhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh”,selected_batches)
# return {
# “total_qty”: flt(total_picked, 3),
# “batches”: selected_batches,
# }
for b in batches:
if total_picked >= required_qty:
print(f"STOP → Target fulfilled ({total_picked})")
break
batch_no = b.get(“batch_no”)
available_qty = flt(b.get(“qty”))
already_used_db = frappe.db.sql(“”"
SELECT IFNULL(SUM(sbe.qty), 0)
FROM \`tabSerial and Batch Entry\` sbe
INNER JOIN \`tabSerial and Batch Bundle\` sb
ON sb.name = sbe.parent
WHERE sbe.batch_no = %s
AND sb.voucher_type='Stock Entry'
AND sb.voucher_no=%s
AND sb.item_code=%s
AND sb.docstatus=1
""", (batch_no, stock_entry_name, item_code))\[0\]\[0\]
already_used_temp = usage_cache.get(batch_no, 0)
effective_used = already_used_db + already_used_temp
remaining_qty = available_qty - effective_used
if remaining_qty <= 0:
print(" SKIPPING (no qty left)\n")
continue
pending_required = required_qty - total_picked
pick_qty = min(pending_required, remaining_qty)
selected_batches.append({
“batch_no”: batch_no,
“picked_qty”: pick_qty,
“available_qty”: available_qty,
})
total_picked += pick_qty
usage_cache[batch_no] = already_used_temp + pick_qty
cache.set_value(cache_key, usage_cache)
return {
“total_qty”: flt(total_picked, 3),
“batches”: selected_batches,
}
except Exception as e:
msg = f"Error fetching batches for item {item_code}: {e}"
frappe.log_error(msg, “Batch Auto Fetch Error”)
frappe.throw(msg)
@frappe.whitelist()
def create_serial_batch_bundle_from_js(parent_doc, child_row, entries):
import frappe
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import \
create_serial_batch_no_ledgers
try:
parent_doc = frappe.parse_json(parent_doc)
child_row = frappe.parse_json(child_row)
entries = frappe.parse_json(entries)
item_code = child_row.get(“item_code”) or “Unknown Item”
existing = child_row.get(“serial_and_batch_bundle”)
warehouse = child_row.get(“s_warehouse”) or child_row.get(“t_warehouse”)
if existing and frappe.db.exists(“Serial and Batch Bundle”, existing):
return {“name”: existing}
bundle_doc = create_serial_batch_no_ledgers(
entries, child_row, parent_doc, warehouse=warehouse
)
frappe.db.set_value(
child_row.get(“doctype”),
child_row.get(“name”),
“serial_and_batch_bundle”,
bundle_doc.name
)
frappe.db.commit()
fatch_uid_from_incoming_iqc(bundle_doc.name, child_row, item_code)
return {“name”: bundle_doc.name}
except Exception as e:
msg = f"Error creating Serial & Batch Bundle for {item_code}: {e}"
frappe.log_error(msg, “Create Bundle Error”)
frappe.throw(msg)
@frappe.whitelist()
def get_all_serials_for_item(item_code, warehouse, required_qty=None):
from frappe.utils import flt
required_qty = flt(required_qty or 0)
if required_qty <= 0:
return {“total_qty”: 0, “serials”: }
serials = frappe.db.sql(“”"
SELECT name AS serial_no, batch_no
FROM \`tabSerial No\`
WHERE item_code = %s
AND warehouse = %s
ORDER BY creation ASC
LIMIT %s
""", (item_code, warehouse, int(required_qty)), as_dict=True)
serial_list =
for s in serials:
serial_list.append({
“serial_no”: s.serial_no,
“batch_no”: s.batch_no,
“picked_qty”: 1
})
return {
“total_qty”: len(serial_list),
“serials”: serial_list
}
# def before_insert(doc, method=None):
# if doc.stock_entry_type:
# if doc.stock_entry_type == “Material Transfer for Manufacture”:
# doc.inspection_required = 0
# elif doc.stock_entry_type == “Manufacture”:
# doc.inspection_required = 1
def before_insert(doc, method=None):
doc.stock_entry_type == “Manufacture”
for row in doc.items:
if row.is_finished_item ==1:
if row.item_code and row.item_code.upper().endswith((“FA”, “FB”, “FC”)):
doc.inspection_required = 1
elif row.item_code and row.item_code.upper().startswith((“FGI”, “FGS”)):
doc.inspection_required = 1
break
def clean_cache_on_delete(doc, method):
KEY_PATTERN = “*STET-25-26-02572*”
cache = frappe.cache()
keys = cache.get_keys(KEY_PATTERN)
if not keys:
return
for key in keys:
real_key = key.decode() if isinstance(key, bytes) else key
cache.delete(real_key)
Could you edit your message and wrap them in code blocks