fix: resolve conflicts

This commit is contained in:
Sudharsanan11
2026-09-21 14:59:08 +05:30
parent 3ee9353240
commit 7ff2a33a5b
4 changed files with 4 additions and 1249 deletions

View File

@@ -1,757 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""Document-mapping and creation helpers for Work Order.
These functions build related documents (Work Order, Stock Entry, Job Card,
Pick List) from a Work Order. They were extracted from work_order.py to slim
the controller; work_order.py re-exports them for backward compatibility.
"""
import json
import math
from functools import partial
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, get_link_to_form, nowdate
from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@frappe.whitelist()
def get_item_details(item: str, project: str | None = None, skip_bom_info: bool = False, throw: bool = True):
frappe.has_permission("Item", "read", throw=True)
res = _item_master_details(item)
if not res:
return {}
if skip_bom_info:
return res
res["bom_no"] = _default_bom_for_item(item, project)
if not res["bom_no"]:
return _handle_missing_default_bom(res, item, project, throw)
_merge_bom_details(res, project)
return res
def _item_master_details(item):
item_table = frappe.qb.DocType("Item")
res = (
frappe.qb.from_(item_table)
.select(
item_table.stock_uom,
item_table.description,
item_table.item_name,
item_table.allow_alternative_item,
item_table.include_item_in_manufacturing,
)
.where((item_table.disabled == 0) & (item_table.name == item) & _item_is_alive(item_table))
).run(as_dict=1)
return res[0] if res else {}
def _item_is_alive(item_table):
# "not set" end_of_life is NULL on postgres (the MariaDB zero-date '0000-00-00' is an invalid
# date constant there), so only add the zero-date term on MariaDB.
is_alive = item_table.end_of_life.isnull() | (item_table.end_of_life > nowdate())
if frappe.db.db_type != "postgres":
is_alive |= item_table.end_of_life == "0000-00-00"
return is_alive
def _default_bom_for_item(item, project):
filters = (
{"item": item, "project": project} if project else {"item": item, "is_default": 1, "docstatus": 1}
)
bom_no = frappe.db.get_value("BOM", filters=filters)
if bom_no:
return bom_no
variant_of = frappe.db.get_value("Item", item, "variant_of")
return frappe.db.get_value("BOM", {"item": variant_of, "is_default": 1}) if variant_of else None
def _handle_missing_default_bom(res, item, project, throw):
if project:
res = get_item_details(item, throw=throw)
frappe.msgprint(
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
)
return res
msg = _("Default BOM for {0} not found").format(item)
frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw))
return res
def _merge_bom_details(res, project):
bom_data = frappe.db.get_value(
"BOM",
res["bom_no"],
["project", "allow_alternative_item", "transfer_material_against", "item_name"],
as_dict=1,
)
res["project"] = project or bom_data.pop("project")
res.update(bom_data)
res.update(check_if_scrap_warehouse_mandatory(res["bom_no"]))
@frappe.whitelist()
def make_work_order(
bom_no: str,
item: str,
qty: float = 0,
company: str | None = None,
project: str | None = None,
variant_items: str | list | None = None,
use_multi_level_bom: bool | None = None,
):
if not frappe.has_permission("Work Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
item_details = get_item_details(item, project)
bom_no = _variant_default_bom(item, bom_no) or bom_no
wo_doc = _new_work_order(item, bom_no, company, item_details, use_multi_level_bom)
if flt(qty) > 0:
wo_doc.qty = flt(qty)
wo_doc.get_items_and_operations_from_bom()
if variant_items and not wo_doc.use_multi_level_bom:
add_variant_item(variant_items, wo_doc, bom_no, "required_items")
return wo_doc
def _variant_default_bom(item, bom_no=None):
if not frappe.db.get_value("Item", item, "variant_of"):
return None
# selected BOM already belongs to this variant
if bom_no and frappe.db.get_value("BOM", bom_no, "item") == item:
return None
return frappe.db.get_value("BOM", {"item": item, "is_default": 1, "docstatus": 1})
def _new_work_order(item, bom_no, company, item_details, use_multi_level_bom):
from erpnext import get_default_company
wo_doc = frappe.new_doc("Work Order")
wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
wo_doc.production_item = item
wo_doc.company = company or get_default_company()
wo_doc.update(item_details)
wo_doc.bom_no = bom_no
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)
return wo_doc
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
variant_items = frappe.parse_json(variant_items)
for item in variant_items:
_add_variant_row(item, wo_doc, bom_no, table_name)
def _add_variant_row(item, wo_doc, bom_no, table_name):
bom_doc = frappe.get_cached_doc("BOM", bom_no)
args = _variant_item_args(item, wo_doc, bom_doc)
existing_row = (
get_template_rm_item(wo_doc, item.get("item_code")) if table_name == "required_items" else None
)
if existing_row:
existing_row.update(args)
else:
wo_doc.append(table_name, args)
def _variant_item_args(item, wo_doc, bom_doc):
args = frappe._dict(
item_code=item.get("variant_item_code"),
required_qty=item.get("qty"),
qty=item.get("qty"), # for bom
source_warehouse=item.get("source_warehouse"),
operation=item.get("operation"),
)
item_data = get_item_details(args.item_code, skip_bom_info=True)
args.update(item_data)
args["rate"] = _variant_item_rate(args, wo_doc, bom_doc)
if not args.source_warehouse:
default = get_item_defaults(item.get("variant_item_code"), wo_doc.company)
args["source_warehouse"] = default.default_warehouse
args["amount"] = flt(args.get("required_qty")) * flt(args.get("rate"))
args["uom"] = item_data.stock_uom
return args
def _variant_item_rate(args, wo_doc, bom_doc):
return get_bom_item_rate(
{
"company": wo_doc.company,
"item_code": args.get("item_code"),
"qty": args.get("required_qty"),
"uom": args.get("stock_uom"),
"stock_uom": args.get("stock_uom"),
"conversion_factor": 1,
},
bom_doc,
)
def get_template_rm_item(wo_doc, item_code):
for row in wo_doc.required_items:
if row.item_code == item_code:
return row
@frappe.whitelist()
def check_if_scrap_warehouse_mandatory(bom_no: str):
frappe.has_permission("BOM", "read", throw=True)
res = {"set_scrap_wh_mandatory": False}
if bom_no:
bom = frappe.get_doc("BOM", bom_no)
if bom.has_scrap_items():
res["set_scrap_wh_mandatory"] = True
return res
@frappe.whitelist()
def make_stock_entry(
work_order_id: str,
purpose: str,
qty: float | None = None,
target_warehouse: str | None = None,
is_additional_transfer_entry: bool = False,
source_stock_entry: str | None = None,
):
frappe.has_permission("Stock Entry", "create", throw=True)
work_order = frappe.get_doc("Work Order", work_order_id)
stock_entry = _new_manufacture_stock_entry(work_order, purpose, qty)
_set_stock_entry_warehouses(stock_entry, work_order, purpose, target_warehouse, source_stock_entry)
stock_entry.set_stock_entry_type()
stock_entry.is_additional_transfer_entry = is_additional_transfer_entry
stock_entry.get_items()
return stock_entry.as_dict()
def _new_manufacture_stock_entry(work_order, purpose, qty):
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = purpose
stock_entry.work_order = work_order.name
stock_entry.company = work_order.company
stock_entry.from_bom = 1
stock_entry.bom_no = work_order.bom_no
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
if purpose in ["Material Transfer for Manufacture", "Manufacture"]:
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
# accept 0 qty as well
stock_entry.fg_completed_qty = (
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
)
return stock_entry
def _set_stock_entry_warehouses(stock_entry, work_order, purpose, target_warehouse, source_stock_entry):
is_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group")
wip_warehouse = None if is_group else work_order.wip_warehouse
stock_entry.project = work_order.project
if purpose == "Material Transfer for Manufacture":
stock_entry.to_warehouse = wip_warehouse
else:
skip = work_order.skip_transfer and not work_order.from_wip_warehouse
stock_entry.from_warehouse = work_order.source_warehouse if skip else wip_warehouse
stock_entry.to_warehouse = work_order.fg_warehouse
if work_order.bom_no:
stock_entry.inspection_required = frappe.db.get_value(
"BOM", work_order.bom_no, "inspection_required"
)
if purpose == "Disassemble":
stock_entry.from_warehouse = work_order.fg_warehouse
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
if source_stock_entry:
stock_entry.source_stock_entry = source_stock_entry
@frappe.whitelist()
def get_fg_conversion_details(work_order: str):
from erpnext.stock.doctype.stock_entry.services.manufacturing import (
get_alternative_finished_goods,
get_converted_fg_qty,
)
if not work_order or not isinstance(work_order, str):
frappe.throw(_("Invalid Work Order"))
frappe.has_permission("Work Order", "read", doc=work_order, throw=True)
wo_details = frappe.db.get_value(
"Work Order", work_order, ["production_item", "produced_qty"], as_dict=True
)
return {
"alternative_items": get_alternative_finished_goods(wo_details.production_item),
"available_qty": flt(wo_details.produced_qty) - get_converted_fg_qty(work_order),
}
@frappe.whitelist()
def make_fg_conversion_entry(work_order: str, item_code: str, qty: float):
if not (work_order and isinstance(work_order, str) and item_code and isinstance(item_code, str)):
frappe.throw(_("Invalid Work Order or Item"))
qty = flt(qty)
if qty <= 0:
frappe.throw(_("The qty to convert must be greater than zero."))
frappe.has_permission("Stock Entry", "create", throw=True)
frappe.has_permission("Work Order", "read", doc=work_order, throw=True)
wo_doc = frappe.get_doc("Work Order", work_order)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = "Repack"
stock_entry.is_fg_conversion = 1
stock_entry.work_order = wo_doc.name
stock_entry.company = wo_doc.company
stock_entry.set_stock_entry_type()
stock_entry.append("items", _get_fg_conversion_row(wo_doc.production_item, qty, wo_doc.fg_warehouse))
target_row = _get_fg_conversion_row(item_code, qty, wo_doc.fg_warehouse, is_target=True)
stock_entry.append("items", target_row)
return stock_entry.as_dict()
def _get_fg_conversion_row(item_code, qty, warehouse, is_target=False):
stock_uom = frappe.get_cached_value("Item", item_code, "stock_uom")
row = {
"item_code": item_code,
"qty": flt(qty),
"transfer_qty": flt(qty),
"uom": stock_uom,
"stock_uom": stock_uom,
"conversion_factor": 1,
"use_serial_batch_fields": 1,
}
if is_target:
row.update({"t_warehouse": warehouse, "is_finished_item": 1})
else:
row["s_warehouse"] = warehouse
return row
@frappe.whitelist()
def make_job_card(work_order: str, operations: str | list, parent_bom: str | None = None):
frappe.has_permission("Job Card", "create", throw=True)
operations = frappe.parse_json(operations)
work_order = frappe.get_doc("Work Order", work_order)
for row in operations:
row = frappe._dict(row)
row.update(get_operation_details(row.name, work_order, parent_bom))
validate_operation_data(row)
qty = row.get("qty")
while qty > 0:
qty = split_qty_based_on_batch_size(work_order, row, qty)
if row.job_card_qty > 0:
create_job_card(work_order, row, auto_create=True)
def get_operation_details(name, work_order, parent_bom):
for row in work_order.operations:
if row.name == name:
return {
"idx": row.idx,
"workstation": row.workstation,
"workstation_type": row.workstation_type,
"source_warehouse": row.source_warehouse,
"fg_warehouse": row.fg_warehouse,
"wip_warehouse": row.wip_warehouse,
"finished_good": row.finished_good,
"bom_no": row.get("bom_no") or parent_bom,
"is_subcontracted": row.get("is_subcontracted"),
}
def split_qty_based_on_batch_size(wo_doc, row, qty):
if not cint(frappe.db.get_value("Operation", row.operation, "create_job_card_based_on_batch_size")):
row.batch_size = row.get("qty") or wo_doc.qty
row.job_card_qty = row.batch_size
if row.batch_size and qty >= row.batch_size:
qty -= row.batch_size
elif qty > 0:
row.job_card_qty = qty
qty = 0
get_serial_nos_for_job_card(row, wo_doc)
return qty
def get_serial_nos_for_job_card(row, wo_doc):
if not wo_doc.has_serial_no:
return
serial_nos = get_serial_nos_for_work_order(wo_doc.name, wo_doc.production_item)
used_serial_nos = []
for d in frappe.get_all(
"Job Card",
fields=["serial_no"],
filters={"docstatus": ("<", 2), "work_order": wo_doc.name, "operation_id": row.name},
):
used_serial_nos.extend(get_serial_nos(d.serial_no))
serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)])
def get_serial_nos_for_work_order(work_order, production_item):
serial_nos = []
for d in frappe.get_all(
"Serial No",
fields=["name"],
filters={
"work_order": work_order,
"item_code": production_item,
},
):
serial_nos.append(d.name)
return serial_nos
def validate_operation_data(row):
if flt(row.get("qty")) <= 0:
frappe.throw(
_("Quantity to Manufacture can not be zero for the operation {0}").format(
frappe.bold(row.get("operation"))
)
)
if flt(row.get("qty")) > flt(row.get("pending_qty")):
frappe.throw(
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity ({2})").format(
frappe.bold(row.get("operation")),
frappe.bold(row.get("qty")),
frappe.bold(row.get("pending_qty")),
)
)
def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False, schedule_blocks=None):
doc = frappe.new_doc("Job Card")
doc.update(_job_card_values(work_order, row))
if work_order.track_semi_finished_goods or (
work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer
):
doc.get_required_items()
if work_order.track_semi_finished_goods:
doc.set_secondary_items()
if auto_create:
_auto_create_job_card(doc, row, enable_capacity_planning, schedule_blocks)
if enable_capacity_planning or schedule_blocks:
# automatically added scheduling rows shouldn't change status to WIP
doc.db_set("status", "Open")
return doc
def _job_card_values(work_order, row):
qty = row.job_card_qty or work_order.get("qty", 0)
values = _job_card_core_values(work_order, row, qty)
values.update(_job_card_warehouse_values(work_order, row, qty))
return values
def _job_card_core_values(work_order, row, qty):
return {
"work_order": work_order.name,
"workstation_type": row.get("workstation_type"),
"operation": row.get("operation"),
"workstation": row.get("workstation"),
"operation_row_id": cint(row.idx),
"posting_date": nowdate(),
"for_quantity": qty,
"operation_id": row.get("name"),
"bom_no": work_order.bom_no,
"project": work_order.project,
"company": work_order.company,
"sequence_id": row.get("sequence_id"),
"hour_rate": row.get("hour_rate"),
}
def _job_card_warehouse_values(work_order, row, qty):
if not work_order.skip_transfer or work_order.from_wip_warehouse:
wip_warehouse = work_order.wip_warehouse or row.get("wip_warehouse")
else:
wip_warehouse = work_order.source_warehouse or row.get("source_warehouse")
return {
"serial_no": row.get("serial_no"),
"time_required": (row.get("time_in_mins", 0) / work_order.qty) * qty,
"source_warehouse": row.get("source_warehouse") or work_order.get("source_warehouse"),
"target_warehouse": row.get("fg_warehouse") or work_order.get("fg_warehouse"),
"wip_warehouse": wip_warehouse,
"skip_material_transfer": row.get("skip_material_transfer"),
"backflush_from_wip_warehouse": row.get("backflush_from_wip_warehouse"),
"finished_good": row.get("finished_good"),
"semi_fg_bom": row.get("bom_no"),
"is_subcontracted": row.get("is_subcontracted"),
"batch_split": row.get("batch_split"),
"weight_per_piece": row.get("weight_per_piece"),
}
def _auto_create_job_card(doc, row, enable_capacity_planning, schedule_blocks=None):
doc.flags.ignore_mandatory = True
if schedule_blocks:
_apply_schedule_blocks(doc, schedule_blocks)
elif enable_capacity_planning:
doc.schedule_time_logs(row)
doc.insert()
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
def _apply_schedule_blocks(doc, schedule_blocks):
if schedule_blocks[0].workstation:
doc.workstation = schedule_blocks[0].workstation
for block in schedule_blocks:
doc.append(
"scheduled_time_logs",
{
"from_time": block.from_time,
"to_time": block.to_time,
"time_in_mins": flt(block.duration_mins),
},
)
def get_work_order_operation_data(work_order, operation, workstation):
for d in work_order.operations:
if d.operation == operation and d.workstation == workstation:
return d
@frappe.whitelist()
def create_pick_list(
source_name: str, target_doc: str | dict | Document | None = None, for_qty: float | None = None
):
frappe.has_permission("Pick List", "create", throw=True)
if for_qty is None:
for_qty = frappe.parse_json(target_doc or "{}").get("for_qty")
for_qty = _validated_for_qty(for_qty)
work_order = frappe.get_doc("Work Order", source_name)
allocation = _allocate_material_demand(work_order, for_qty / flt(work_order.qty))
postprocess = partial(_set_pick_list_item_qty, allocation_by_item=allocation)
doc = get_mapped_doc("Work Order", source_name, _pick_list_mapping(postprocess, allocation), target_doc)
_validate_material_is_pending(doc.locations)
doc.purpose = "Material Transfer for Manufacture"
doc.for_qty = for_qty
if not doc.pick_manually:
doc.set_item_locations()
return doc
def _pick_list_mapping(postprocess, allocation):
return {
"Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
"Work Order Item": {
"doctype": "Pick List Item",
"field_no_map": ["transferred_qty"],
"postprocess": postprocess,
"condition": lambda doc: _allocation_key(doc) in allocation,
},
}
def _allocate_material_demand(work_order, fraction):
"""Fraction of each (item, warehouse, operation row) group's requirement, capped
at the group's proportional share of the item's pending pool."""
required_by_item = {}
covered_by_item = {}
required_by_group = {}
for row in work_order.required_items:
required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty)
covered_by_item.setdefault(
row.item_code,
flt(row.transferred_qty) + flt(row.requested_qty) + flt(row.picked_qty),
)
key = _allocation_key(row)
required_by_group[key] = required_by_group.get(key, 0.0) + flt(row.required_qty)
pending_pool = {
item_code: required_qty - covered_by_item[item_code]
for item_code, required_qty in required_by_item.items()
}
allocation = {}
for key, required_qty in required_by_group.items():
item_code = key[0]
if required_by_item[item_code] <= 0:
continue
pool_share = pending_pool[item_code] * required_qty / required_by_item[item_code]
qty = min(required_qty * fraction, pool_share)
if qty > 0:
allocation[key] = qty
return allocation
def _allocation_key(row):
"""Manual rows have no operation_row_id; their operation label splits them."""
return (row.item_code, row.source_warehouse, cint(row.operation_row_id) or row.operation)
def _merge_allocation_per_item(allocation):
"""Material Request rejects repeated item codes unless Buying Settings allows them."""
merged = {}
key_by_item = {}
for key, qty in allocation.items():
item_code = key[0]
if item_code in key_by_item:
merged[key_by_item[item_code]] += qty
else:
key_by_item[item_code] = key
merged[key] = qty
return merged
def _validated_for_qty(for_qty):
qty = flt(for_qty)
if not math.isfinite(qty) or qty <= 0:
frappe.throw(_("Quantity must be greater than zero."))
return qty
def _validate_material_is_pending(rows):
if not rows:
frappe.throw(
_("All required items have already been transferred, requested or picked."),
title=_("No Pending Materials"),
)
def _set_pick_list_item_qty(source, target, source_parent, allocation_by_item):
qty = allocation_by_item.pop(_allocation_key(source), 0.0)
if qty <= 0:
target.delete()
return
target.qty = qty
target.stock_qty = qty
target.uom = frappe.get_value("Item", source.item_code, "stock_uom")
target.stock_uom = target.uom
target.conversion_factor = 1
@frappe.whitelist()
def make_material_request(
source_name: str, target_doc: str | dict | Document | None = None, for_qty: float | None = None
):
frappe.has_permission("Material Request", "create", throw=True)
if for_qty is None and frappe.flags.args:
for_qty = frappe.flags.args.for_qty
work_order = frappe.get_doc("Work Order", source_name)
fraction = 1.0
if for_qty is not None:
fraction = _validated_for_qty(for_qty) / flt(work_order.qty)
allocation = _allocate_material_demand(work_order, fraction)
if not cint(frappe.db.get_single_value("Buying Settings", "allow_multiple_items")):
allocation = _merge_allocation_per_item(allocation)
postprocess = partial(_set_material_request_item, allocation_by_item=allocation)
doc = get_mapped_doc(
"Work Order", source_name, _material_request_mapping(postprocess, allocation), target_doc
)
_validate_material_is_pending(doc.items)
doc.material_request_type = "Material Transfer"
return doc
def _material_request_mapping(postprocess, allocation):
return {
"Work Order": {
"doctype": "Material Request",
"validation": {"docstatus": ["=", 1]},
"field_map": {"name": "work_order"},
},
"Work Order Item": {
"doctype": "Material Request Item",
"field_map": [
("stock_uom", "uom"),
("source_warehouse", "from_warehouse"),
],
"postprocess": postprocess,
"condition": lambda doc: _allocation_key(doc) in allocation,
},
}
def _set_material_request_item(source, target, source_parent, allocation_by_item):
qty = allocation_by_item.pop(_allocation_key(source), 0.0)
if qty <= 0:
target.delete()
return
target.warehouse = source_parent.wip_warehouse
target.qty = qty
target.schedule_date = nowdate()
@frappe.whitelist()
def make_stock_return_entry(work_order: str):
from erpnext.stock.doctype.stock_entry.services.manufacturing import (
ManufactureStockEntry,
)
frappe.has_permission("Stock Entry", "create", throw=True)
wo_doc = frappe.get_cached_doc("Work Order", work_order)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.from_bom = 1
stock_entry.is_return = 1
stock_entry.work_order = work_order
stock_entry.purpose = "Material Transfer for Manufacture"
stock_entry.bom_no = wo_doc.bom_no
stock_entry.set_stock_entry_type()
ste_cls = ManufactureStockEntry(stock_entry)
ste_cls.add_raw_materials_based_on_transfer()
ste_cls.return_available_materials_in_source_wh()
return stock_entry

View File

@@ -3069,7 +3069,8 @@ def create_pick_list(source_name: str, target_doc: str | dict | None = None, for
doc.purpose = "Material Transfer for Manufacture"
doc.for_qty = for_qty
doc.set_item_locations()
if not doc.pick_manually:
doc.set_item_locations()
return doc

View File

@@ -1,490 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, comma_and, flt, get_link_to_form, getdate, nowdate
from erpnext.controllers.mapper import get_qty_already_mapped
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_supplier
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
get_subcontracting_boms_for_finished_goods,
)
def set_missing_values(source, target_doc):
if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()):
target_doc.schedule_date = None
target_doc.run_method("set_missing_values")
target_doc.run_method("calculate_taxes_and_totals")
def get_source_item_for_qty(item, qty):
"""Copy of the source row whose pending quantity is the requested quantity."""
source_item = frappe._dict(item.as_dict())
source_item.ordered_qty = 0
source_item.received_qty = 0
source_item.stock_qty = flt(qty) * flt(item.conversion_factor)
return source_item
def update_item(obj, target, source_parent):
target.conversion_factor = obj.conversion_factor
qty = obj.ordered_qty or obj.received_qty
target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor
target.stock_qty = target.qty * target.conversion_factor
if getdate(target.schedule_date) < getdate(nowdate()):
target.schedule_date = None
if target.fg_item:
target.fg_item_qty = obj.stock_qty
if sc_bom := get_subcontracting_boms_for_finished_goods(target.fg_item):
target.item_code = sc_bom.service_item
target.uom = sc_bom.service_item_uom
target.conversion_factor = (
frappe.db.get_value(
"UOM Conversion Detail",
{"parent": sc_bom.service_item, "uom": sc_bom.service_item_uom},
"conversion_factor",
)
or 1
)
target.qty = target.fg_item_qty * sc_bom.conversion_factor
target.stock_qty = target.qty * target.conversion_factor
@frappe.whitelist()
def make_purchase_order(
source_name: str, target_doc: str | dict | Document | None = None, args: dict | str | None = None
):
if args is None:
args = frappe.flags.args or {}
args = frappe.parse_json(args)
is_subcontracted = (
frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
)
requested_qty = args.get("requested_qty") or {}
mapped_qty_by_item = get_qty_already_mapped(target_doc, "material_request_item", "stock_qty")
def postprocess(source, target_doc):
target_doc.is_subcontracted = is_subcontracted
if args.get("supplier"):
target_doc.supplier = args.get("supplier")
set_missing_values(source, target_doc)
def update_requested_item(obj, target, source_parent):
if obj.name in requested_qty:
obj = get_source_item_for_qty(obj, requested_qty[obj.name])
update_item(obj, target, source_parent)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
qty = (d.ordered_qty or d.received_qty) + flt(mapped_qty_by_item.get(d.name, 0))
return qty < d.stock_qty and child_filter
def generate_field_map():
field_map = [
["name", "material_request_item"],
["parent", "material_request"],
["sales_order", "sales_order"],
["sales_order_item", "sales_order_item"],
["wip_composite_asset", "wip_composite_asset"],
]
if is_subcontracted:
field_map.extend([["item_code", "fg_item"], ["qty", "fg_item_qty"]])
else:
field_map.extend([["uom", "stock_uom"], ["uom", "uom"]])
return field_map
doclist = get_mapped_doc(
"Material Request",
source_name,
{
"Material Request": {
"doctype": "Purchase Order",
"validation": {
"docstatus": ["=", 1],
"material_request_type": ["in", ["Purchase", "Subcontracting"]],
},
},
"Material Request Item": {
"doctype": "Purchase Order Item",
"field_map": generate_field_map(),
"field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
"postprocess": update_requested_item,
"condition": select_item,
},
},
target_doc,
postprocess,
)
doclist.set_onload("load_after_mapping", False)
return doclist
@frappe.whitelist()
def make_request_for_quotation(source_name: str, target_doc: str | dict | Document | None = None):
def update_item(obj, target, source_parent):
qty = obj.ordered_qty or obj.received_qty
target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor
target.stock_qty = target.qty * target.conversion_factor
def select_item(d):
qty = d.ordered_qty or d.received_qty
return qty < d.stock_qty
doclist = get_mapped_doc(
"Material Request",
source_name,
{
"Material Request": {
"doctype": "Request for Quotation",
"validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
},
"Material Request Item": {
"doctype": "Request for Quotation Item",
"field_map": [
["name", "material_request_item"],
["parent", "material_request"],
["project", "project_name"],
["cost_center", "cost_center"],
],
"postprocess": update_item,
"condition": select_item,
},
},
target_doc,
)
return doclist
def get_default_supplier_for_item(item_code: str, company: str) -> str | None:
return get_default_supplier(
frappe._dict(),
get_item_defaults(item_code, company),
get_item_group_defaults(item_code, company),
get_brand_defaults(item_code, company),
)
@frappe.whitelist()
def get_item_default_suppliers(source_name: str, filtered_children: str | list | None = None) -> list[dict]:
"""Pending items of the Material Request with their default supplier."""
filtered_children = frappe.parse_json(filtered_children) if filtered_children else []
material_request = frappe.get_doc("Material Request", source_name)
material_request.check_permission("read")
items = []
for item in material_request.items:
if filtered_children and item.name not in filtered_children:
continue
ordered_qty = flt(item.ordered_qty) or flt(item.received_qty)
if ordered_qty >= flt(item.stock_qty):
continue
items.append(
{
"material_request_item": item.name,
"item_code": item.item_code,
"item_name": item.item_name,
"pending_qty": (flt(item.stock_qty) - ordered_qty) / (flt(item.conversion_factor) or 1),
"uom": item.uom,
"supplier": get_default_supplier_for_item(item.item_code, material_request.company),
}
)
return items
@frappe.whitelist(methods=["POST"])
def make_purchase_orders_by_supplier(source_name: str, item_suppliers: str | list) -> list[str]:
"""Create one draft Purchase Order per supplier for the given Material Request items."""
item_suppliers = frappe.parse_json(item_suppliers)
if not item_suppliers:
frappe.throw(_("Select at least one Item"))
pending_items = {
d["material_request_item"]: frappe._dict(d) for d in get_item_default_suppliers(source_name)
}
items_by_supplier = {}
requested_items = set()
for row in item_suppliers:
row = frappe._dict(row)
pending = pending_items.get(row.material_request_item) or frappe._dict()
item_link = get_link_to_form("Item", row.item_code)
if row.material_request_item in requested_items:
frappe.throw(_("Item {0} cannot be ordered more than once").format(item_link))
requested_items.add(row.material_request_item)
if not row.supplier:
frappe.throw(_("Select a Supplier for Item {0}").format(item_link))
if flt(row.qty) <= 0 or flt(row.qty) > flt(pending.pending_qty):
pending_qty = frappe.format_value(flt(pending.pending_qty), "Float")
frappe.throw(
_("Quantity for Item {0} must be greater than zero and cannot exceed {1}").format(
item_link, frappe.bold(f"{pending_qty} {pending.uom or ''}".strip())
)
)
items_by_supplier.setdefault(row.supplier, {})[row.material_request_item] = flt(row.qty)
purchase_orders = []
is_rescheduled = False
for supplier, requested_qty in items_by_supplier.items():
purchase_order = make_purchase_order(
source_name,
args={
"supplier": supplier,
"filtered_children": list(requested_qty),
"requested_qty": requested_qty,
},
)
for item in purchase_order.items:
if not item.schedule_date:
item.schedule_date = nowdate()
is_rescheduled = True
purchase_order.insert()
purchase_orders.append(purchase_order.name)
if is_rescheduled:
frappe.toast(
_("{0} was set to today for items whose requested date has passed").format(
_(frappe.get_meta("Purchase Order Item").get_label("schedule_date"))
),
indicator="orange",
)
if len(purchase_orders) > 1:
frappe.msgprint(
_("{0} created").format(
comma_and([get_link_to_form("Purchase Order", name) for name in purchase_orders])
)
)
return purchase_orders
@frappe.whitelist()
def get_items_based_on_default_supplier(supplier: str):
supplier_items = [
d.parent
for d in frappe.db.get_all(
"Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent"
)
]
return supplier_items
@frappe.whitelist()
def make_supplier_quotation(source_name: str, target_doc: str | dict | Document | None = None):
def postprocess(source, target_doc):
set_missing_values(source, target_doc)
doclist = get_mapped_doc(
"Material Request",
source_name,
{
"Material Request": {
"doctype": "Supplier Quotation",
"validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
},
"Material Request Item": {
"doctype": "Supplier Quotation Item",
"field_map": {
"name": "material_request_item",
"parent": "material_request",
"sales_order": "sales_order",
},
},
},
target_doc,
postprocess,
)
doclist.set_onload("load_after_mapping", False)
return doclist
@frappe.whitelist()
def make_stock_entry(source_name: str, target_doc: str | dict | Document | None = None):
def update_item(obj, target, source_parent):
qty = (
flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor
if flt(obj.stock_qty) > flt(obj.ordered_qty)
else 0
)
target.qty = qty
target.transfer_qty = qty * obj.conversion_factor
target.conversion_factor = obj.conversion_factor
if (
source_parent.material_request_type == "Material Transfer"
or source_parent.material_request_type == "Customer Provided"
):
target.t_warehouse = obj.warehouse
else:
target.s_warehouse = obj.warehouse
if source_parent.material_request_type == "Customer Provided":
target.allow_zero_valuation_rate = 1
if source_parent.material_request_type == "Material Transfer":
target.s_warehouse = obj.from_warehouse
def set_missing_values(source, target):
target.purpose = source.material_request_type
target.from_warehouse = source.set_from_warehouse
target.to_warehouse = source.set_warehouse
if source.material_request_type == "Material Issue":
target.from_warehouse = source.set_warehouse
target.to_warehouse = None
if source.job_card:
target.purpose = "Material Transfer for Manufacture"
if source.work_order:
target.purpose = "Material Transfer for Manufacture"
if source.material_request_type == "Customer Provided":
target.purpose = "Material Receipt"
target.set_transfer_qty()
target.set_actual_qty()
target.calculate_rate_and_amount(raise_error_if_no_rate=False)
target.stock_entry_type = target.purpose
if source.job_card:
job_card_details = frappe.get_all(
"Job Card", filters={"name": source.job_card}, fields=["bom_no", "for_quantity"]
)
if job_card_details and job_card_details[0]:
target.bom_no = job_card_details[0].bom_no
target.fg_completed_qty = job_card_details[0].for_quantity
target.from_bom = 1
if source.work_order:
work_order_details = frappe.db.get_value(
"Work Order", source.work_order, ["bom_no", "use_multi_level_bom"], as_dict=True
)
if work_order_details:
target.bom_no = work_order_details.bom_no
target.use_multi_level_bom = work_order_details.use_multi_level_bom
target.from_bom = 1
# not fg-qty-driven, mirrors the Pick List -> Stock Entry transfer for this Work Order
target.fg_completed_qty = 0
doclist = get_mapped_doc(
"Material Request",
source_name,
{
"Material Request": {
"doctype": "Stock Entry",
"validation": {
"docstatus": ["=", 1],
"material_request_type": [
"in",
["Material Transfer", "Material Issue", "Customer Provided"],
],
},
},
"Material Request Item": {
"doctype": "Stock Entry Detail",
"field_map": {
"name": "material_request_item",
"parent": "material_request",
"uom": "stock_uom",
"job_card_item": "job_card_item",
},
"field_no_map": ["expense_account"],
"postprocess": update_item,
"condition": lambda doc: (
flt(doc.ordered_qty, doc.precision("ordered_qty"))
< flt(doc.stock_qty, doc.precision("ordered_qty"))
),
},
},
target_doc,
set_missing_values,
)
return doclist
@frappe.whitelist()
def create_pick_list(source_name: str, target_doc: str | dict | Document | None = None):
def update_item(obj, target, source_parent):
qty = flt((obj.stock_qty - obj.picked_qty) / target.conversion_factor, obj.precision("qty"))
target.qty = qty
target.stock_qty = qty * obj.conversion_factor
target.conversion_factor = obj.conversion_factor
doc = get_mapped_doc(
"Material Request",
source_name,
{
"Material Request": {
"doctype": "Pick List",
"field_map": {"material_request_type": "purpose"},
"validation": {"docstatus": ["=", 1]},
},
"Material Request Item": {
"doctype": "Pick List Item",
"field_map": {
"name": "material_request_item",
"stock_qty": "stock_qty",
"from_warehouse": "warehouse",
},
"postprocess": update_item,
"condition": lambda doc: (
flt(doc.picked_qty, doc.precision("picked_qty"))
< flt(doc.stock_qty, doc.precision("stock_qty"))
),
},
},
target_doc,
)
if not doc.pick_manually:
doc.set_item_locations()
return doc
@frappe.whitelist()
def make_in_transit_stock_entry(source_name: str, in_transit_warehouse: str):
ste_doc = make_stock_entry(source_name)
ste_doc.add_to_transit = 1
ste_doc.to_warehouse = in_transit_warehouse
for row in ste_doc.items:
row.t_warehouse = in_transit_warehouse
return ste_doc

View File

@@ -1156,7 +1156,8 @@ def create_pick_list(source_name, target_doc=None):
target_doc,
)
doc.set_item_locations()
if not doc.pick_manually:
doc.set_item_locations()
return doc