Skip to content

Commit

Permalink
feat: manufacturing over/under production
Browse files Browse the repository at this point in the history
* feat: WIP Manufacturing Over/Under Production

* feat: WIP Manufacturing Over/Under Production

* feat: WIP Manufacturing Over/Under Production

* fix: unused import

* feat: override onload for work order

* feat: oveeride get_pending_raw_materials

* fix: allowed_qty in job card

* fix: indentation

* fix: import

* fixes

* wip: tests

* feat: test_get_allowance_percentage

* feat: test test_validate_finished_goods

* fix: validate_job_card and test

* fix: test
  • Loading branch information
fproldan authored Feb 1, 2024
1 parent 5bf9486 commit baeb469
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 3 deletions.
2 changes: 2 additions & 0 deletions inventory_tools/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@
"Purchase Order": "inventory_tools.inventory_tools.overrides.purchase_order.InventoryToolsPurchaseOrder",
"Purchase Receipt": "inventory_tools.inventory_tools.overrides.purchase_receipt.InventoryToolsPurchaseReceipt",
"Production Plan": "inventory_tools.inventory_tools.overrides.production_plan.InventoryToolsProductionPlan",
"Stock Entry": "inventory_tools.inventory_tools.overrides.stock_entry.InventoryToolsStockEntry",
"Job Card": "inventory_tools.inventory_tools.overrides.job_card.InventoryToolsJobCard",
}


Expand Down
63 changes: 62 additions & 1 deletion inventory_tools/inventory_tools/custom/bom.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 11,
"idx": 12,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
Expand Down Expand Up @@ -121,6 +121,67 @@
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2024-01-11 10:05:40.558053",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "BOM",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "overproduction_percentage_for_work_order",
"fieldtype": "Percent",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 12,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "create_job_cards_automatically",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Overproduction Percentage For Work Order",
"length": 0,
"mandatory_depends_on": null,
"modified": "2024-01-11 10:07:40.558053",
"modified_by": "Administrator",
"module": "Inventory Tools",
"name": "BOM-overproduction_percentage_for_work_order",
"no_copy": 0,
"non_negative": 0,
"options": null,
"owner": "Administrator",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"create_purchase_orders",
"bom_column",
"create_job_cards_automatically",
"column_break_ilobm",
"overproduction_percentage_for_work_order",
"section_break_0",
"update_warehouse_path",
"section_break_gzcbr",
Expand Down Expand Up @@ -108,11 +110,20 @@
"fieldtype": "Select",
"label": "Create Job Card(s) Automatically",
"options": "Yes\nNo"
},
{
"fieldname": "column_break_ilobm",
"fieldtype": "Column Break"
},
{
"fieldname": "overproduction_percentage_for_work_order",
"fieldtype": "Percent",
"label": "Overproduction Percentage For Work Order"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-01-09 18:12:20.714002",
"modified": "2024-01-11 10:10:57.356068",
"modified_by": "Administrator",
"module": "Inventory Tools",
"name": "Inventory Tools Settings",
Expand Down
42 changes: 42 additions & 0 deletions inventory_tools/inventory_tools/overrides/job_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import frappe
from erpnext.manufacturing.doctype.job_card.job_card import JobCard
from frappe import _, bold
from frappe.utils import get_link_to_form

from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage


class InventoryToolsJobCard(JobCard):
def validate_job_card(self):
if (
self.work_order
and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped"
):
frappe.throw(
_("Transaction not allowed against stopped Work Order {0}").format(
get_link_to_form("Work Order", self.work_order)
)
)

if not self.time_logs:
frappe.throw(
_("Time logs are required for {0} {1}").format(
bold("Job Card"), get_link_to_form("Job Card", self.name)
)
)

# don't validate mfg qty so partial consumption can take place
# PATCH: use manufacturing settings overproduction percentage to allow overproduction on Job Card
overproduction_percentage = get_allowance_percentage(self.company, self.bom_no)
allowed_qty = self.for_quantity * (1 + overproduction_percentage / 100)
if self.for_quantity and self.total_completed_qty > allowed_qty:
total_completed_qty = frappe.bold(frappe._("Total Completed Qty"))
qty_to_manufacture = frappe.bold(frappe._("Qty to Manufacture"))
frappe.throw(
frappe._("The {0} ({1}) must be equal to {2} ({3})").format(
total_completed_qty,
frappe.bold(self.total_completed_qty),
qty_to_manufacture,
frappe.bold(self.for_quantity),
)
)
153 changes: 153 additions & 0 deletions inventory_tools/inventory_tools/overrides/stock_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import frappe
from erpnext.stock.doctype.stock_entry.stock_entry import FinishedGoodError, StockEntry
from frappe import _
from frappe.utils import flt

from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage


class InventoryToolsStockEntry(StockEntry):
def check_if_operations_completed(self):
"""
Original code checks that the stock entry amount plus what's already produced in the WO
is not larger than any operation's completed quantity (plus the overallowance amount).
Since customized code rewires so stock entries happen via a Job Card, the function now
checks that the stock entry amount plus what's already been produced in the WO is not
greater than the amount to be manufactured plus the overallowance amount.
"""
prod_order = frappe.get_doc("Work Order", self.work_order)
allowance_percentage = get_allowance_percentage(self.company, self.bom_no)

jc_qty = flt(
self.fg_completed_qty
) # quantity manufactured and being entered in stock entry for this JC
already_produced = flt(prod_order.produced_qty) # quantity already manufactured for WO
total_completed_qty = jc_qty + already_produced

wo_to_man_qty = flt(prod_order.qty)
allowed_qty = wo_to_man_qty * (
1 + allowance_percentage / 100
) # amount to be manufactured on the WO including the overallowance amount

if total_completed_qty > allowed_qty:
work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
frappe.throw(
_(
"Quantity manufactured in this Job Card of {0} plus quantity already produced for Work Order {1} of {2} is greater than the Work Order's quantity to manufacture of {3} plus the overproduction allowance of {4}%"
).format(
self.fg_completed_qty,
work_order_link,
already_produced,
wo_to_man_qty,
allowance_percentage,
)
)

def validate_finished_goods(self):
"""
1. Check if FG exists (mfg, repack)
2. Check if Multiple FG Items are present (mfg)
3. Check FG Item and Qty against WO if present (mfg)
"""
production_item, wo_qty, finished_items = None, 0, []

wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"])
if wo_details:
production_item, wo_qty = wo_details

for d in self.get("items"):
if d.is_finished_item:
if not self.work_order:
# Independent MFG Entry/ Repack Entry, no WO to match against
finished_items.append(d.item_code)
continue

if d.item_code != production_item:
frappe.throw(
_("Finished Item {0} does not match with Work Order {1}").format(
d.item_code, self.work_order
)
)
elif flt(d.qty) > flt(self.fg_completed_qty):
frappe.throw(
_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format(
d.idx, d.qty, self.fg_completed_qty
)
)

finished_items.append(d.item_code)

if not finished_items:
frappe.throw(
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
title=_("Missing Finished Good"),
exc=FinishedGoodError,
)

if self.purpose == "Manufacture":
if len(set(finished_items)) > 1:
frappe.throw(
msg=_("Multiple items cannot be marked as finished item"),
title=_("Note"),
exc=FinishedGoodError,
)

allowance_percentage = get_allowance_percentage(self.company, self.bom_no)
allowed_qty = wo_qty + ((allowance_percentage / 100) * wo_qty)

# No work order could mean independent Manufacture entry, if so skip validation
if self.work_order and self.fg_completed_qty > allowed_qty:
frappe.throw(
_("For quantity {0} should not be greater than work order quantity {1}").format(
flt(self.fg_completed_qty), wo_qty
)
)

def get_pending_raw_materials(self, backflush_based_on=None):
"""
issue (item quantity) that is pending to issue or desire to transfer,
whichever is less
"""
item_dict = self.get_pro_order_required_items(backflush_based_on)

max_qty = flt(self.pro_doc.qty)

allow_overproduction = False
overproduction_percentage = get_allowance_percentage(self.company, self.bom_no)

to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(
self.fg_completed_qty
)
transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100)

if transfer_limit_qty >= to_transfer_qty:
allow_overproduction = True

for item, item_details in item_dict.items():
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty

if (
desire_to_transfer <= pending_to_issue
or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture")
or allow_overproduction
):
# "No need for transfer but qty still pending to transfer" case can occur
# when transferring multiple RM in different Stock Entries
item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue
elif pending_to_issue > 0:
item_dict[item]["qty"] = pending_to_issue
else:
item_dict[item]["qty"] = 0

# delete items with 0 qty
list_of_items = list(item_dict.keys())
for item in list_of_items:
if not item_dict[item]["qty"]:
del item_dict[item]

# show some message
if not len(item_dict):
frappe.msgprint(_("""All items have already been transferred for this Work Order."""))

return item_dict
Loading

0 comments on commit baeb469

Please sign in to comment.