Merge pull request #27475 from marination/job-card-excess-transfer
fix: Handle Excess/Multiple Item Transfer against Job Card
This commit is contained in:
commit
8fed8da9eb
@ -26,15 +26,23 @@ frappe.ui.form.on('Job Card', {
|
|||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frappe.flags.pause_job = 0;
|
frappe.flags.pause_job = 0;
|
||||||
frappe.flags.resume_job = 0;
|
frappe.flags.resume_job = 0;
|
||||||
|
let has_items = frm.doc.items && frm.doc.items.length;
|
||||||
|
|
||||||
if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) {
|
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
|
||||||
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
|
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
||||||
|
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||||
|
|
||||||
|
if (to_request || excess_transfer_allowed) {
|
||||||
frm.add_custom_button(__("Material Request"), () => {
|
frm.add_custom_button(__("Material Request"), () => {
|
||||||
frm.trigger("make_material_request");
|
frm.trigger("make_material_request");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
|
// check if any row has untransferred materials
|
||||||
|
// in case of multiple items in JC
|
||||||
|
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
||||||
|
|
||||||
|
if (to_transfer || excess_transfer_allowed) {
|
||||||
frm.add_custom_button(__("Material Transfer"), () => {
|
frm.add_custom_button(__("Material Transfer"), () => {
|
||||||
frm.trigger("make_stock_entry");
|
frm.trigger("make_stock_entry");
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
|
@ -185,7 +185,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "transferred_qty",
|
"fieldname": "transferred_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Transferred Qty",
|
"label": "FG Qty from Transferred Raw Materials",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -396,10 +396,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-16 15:59:32.766484",
|
"modified": "2021-09-13 21:34:15.177928",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -37,6 +34,10 @@ class OperationSequenceError(frappe.ValidationError): pass
|
|||||||
class JobCardCancelError(frappe.ValidationError): pass
|
class JobCardCancelError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class JobCard(Document):
|
class JobCard(Document):
|
||||||
|
def onload(self):
|
||||||
|
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||||
|
self.set_onload("job_card_excess_transfer", excess_transfer)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_time_logs()
|
self.validate_time_logs()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
@ -449,6 +450,7 @@ class JobCard(Document):
|
|||||||
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
|
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
|
||||||
|
|
||||||
def set_transferred_qty(self, update_status=False):
|
def set_transferred_qty(self, update_status=False):
|
||||||
|
"Set total FG Qty for which RM was transferred."
|
||||||
if not self.items:
|
if not self.items:
|
||||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||||
|
|
||||||
@ -457,6 +459,7 @@ class JobCard(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.items:
|
if self.items:
|
||||||
|
# sum of 'For Quantity' of Stock Entries against JC
|
||||||
self.transferred_qty = frappe.db.get_value('Stock Entry', {
|
self.transferred_qty = frappe.db.get_value('Stock Entry', {
|
||||||
'job_card': self.name,
|
'job_card': self.name,
|
||||||
'work_order': self.work_order,
|
'work_order': self.work_order,
|
||||||
@ -500,7 +503,9 @@ class JobCard(Document):
|
|||||||
self.status = 'Work In Progress'
|
self.status = 'Work In Progress'
|
||||||
|
|
||||||
if (self.docstatus == 1 and
|
if (self.docstatus == 1 and
|
||||||
(self.for_quantity == self.transferred_qty or not self.items)):
|
(self.for_quantity <= self.transferred_qty or not self.items)):
|
||||||
|
# consider excess transfer
|
||||||
|
# completed qty is checked via separate validation
|
||||||
self.status = 'Completed'
|
self.status = 'Completed'
|
||||||
|
|
||||||
if self.status != 'Completed':
|
if self.status != 'Completed':
|
||||||
@ -618,7 +623,11 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.purpose = "Material Transfer for Manufacture"
|
target.purpose = "Material Transfer for Manufacture"
|
||||||
target.from_bom = 1
|
target.from_bom = 1
|
||||||
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
|
|
||||||
|
# avoid negative 'For Quantity'
|
||||||
|
pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
|
||||||
|
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
||||||
|
|
||||||
target.set_transfer_qty()
|
target.set_transfer_qty()
|
||||||
target.calculate_rate_and_amount()
|
target.calculate_rate_and_amount()
|
||||||
target.set_missing_values()
|
target.set_missing_values()
|
||||||
|
@ -1,22 +1,38 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import random_string
|
from frappe.utils import random_string
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
|
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||||
|
make_stock_entry as make_stock_entry_from_jc,
|
||||||
|
OperationMismatchError,
|
||||||
|
OverlapError
|
||||||
|
)
|
||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
class TestJobCard(unittest.TestCase):
|
class TestJobCard(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
transfer_material_against, source_warehouse = None, None
|
||||||
|
tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
|
||||||
|
"test_job_card_excess_material_transfer")
|
||||||
|
|
||||||
|
if self._testMethodName in tests_that_transfer_against_jc:
|
||||||
|
transfer_material_against = "Job Card"
|
||||||
|
source_warehouse = "Stores - _TC"
|
||||||
|
|
||||||
|
self.work_order = make_wo_order_test_record(
|
||||||
|
item="_Test FG Item 2",
|
||||||
|
qty=2,
|
||||||
|
transfer_material_against=transfer_material_against,
|
||||||
|
source_warehouse=source_warehouse
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@ -96,3 +112,84 @@ class TestJobCard(unittest.TestCase):
|
|||||||
"employee": employee,
|
"employee": employee,
|
||||||
})
|
})
|
||||||
self.assertRaises(OverlapError, jc2.save)
|
self.assertRaises(OverlapError, jc2.save)
|
||||||
|
|
||||||
|
def test_job_card_multiple_materials_transfer(self):
|
||||||
|
"Test transferring RMs separately against Job Card with multiple RMs."
|
||||||
|
make_stock_entry(
|
||||||
|
item_code="_Test Item",
|
||||||
|
target="Stores - _TC",
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100
|
||||||
|
)
|
||||||
|
make_stock_entry(
|
||||||
|
item_code="_Test Item Home Desktop Manufactured",
|
||||||
|
target="Stores - _TC",
|
||||||
|
qty=6,
|
||||||
|
basic_rate=100
|
||||||
|
)
|
||||||
|
|
||||||
|
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
|
||||||
|
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||||
|
|
||||||
|
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||||
|
del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
|
||||||
|
transfer_entry_1.insert()
|
||||||
|
transfer_entry_1.submit()
|
||||||
|
|
||||||
|
job_card.reload()
|
||||||
|
|
||||||
|
self.assertEqual(transfer_entry_1.fg_completed_qty, 2)
|
||||||
|
self.assertEqual(job_card.transferred_qty, 2)
|
||||||
|
|
||||||
|
# transfer second RM
|
||||||
|
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
||||||
|
del transfer_entry_2.items[0]
|
||||||
|
transfer_entry_2.insert()
|
||||||
|
transfer_entry_2.submit()
|
||||||
|
|
||||||
|
# 'For Quantity' here will be 0 since
|
||||||
|
# transfer was made for 2 fg qty in first transfer Stock Entry
|
||||||
|
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
||||||
|
|
||||||
|
def test_job_card_excess_material_transfer(self):
|
||||||
|
"Test transferring more than required RM against Job Card."
|
||||||
|
make_stock_entry(item_code="_Test Item", target="Stores - _TC",
|
||||||
|
qty=25, basic_rate=100)
|
||||||
|
make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
|
||||||
|
target="Stores - _TC", qty=15, basic_rate=100)
|
||||||
|
|
||||||
|
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
|
||||||
|
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||||
|
|
||||||
|
# fully transfer both RMs
|
||||||
|
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||||
|
transfer_entry_1.insert()
|
||||||
|
transfer_entry_1.submit()
|
||||||
|
|
||||||
|
# transfer extra qty of both RM due to previously damaged RM
|
||||||
|
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
||||||
|
# deliberately change 'For Quantity'
|
||||||
|
transfer_entry_2.fg_completed_qty = 1
|
||||||
|
transfer_entry_2.items[0].qty = 5
|
||||||
|
transfer_entry_2.items[1].qty = 3
|
||||||
|
transfer_entry_2.insert()
|
||||||
|
transfer_entry_2.submit()
|
||||||
|
|
||||||
|
job_card.reload()
|
||||||
|
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
||||||
|
|
||||||
|
# Check if 'For Quantity' is negative
|
||||||
|
# as 'transferred_qty' > Qty to Manufacture
|
||||||
|
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
|
||||||
|
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
||||||
|
|
||||||
|
job_card.append("time_logs", {
|
||||||
|
"from_time": "2021-01-01 00:01:00",
|
||||||
|
"to_time": "2021-01-01 06:00:00",
|
||||||
|
"completed_qty": 2
|
||||||
|
})
|
||||||
|
job_card.save()
|
||||||
|
job_card.submit()
|
||||||
|
|
||||||
|
# JC is Completed with excess transfer
|
||||||
|
self.assertEqual(job_card.status, "Completed")
|
@ -25,9 +25,12 @@
|
|||||||
"overproduction_percentage_for_sales_order",
|
"overproduction_percentage_for_sales_order",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"overproduction_percentage_for_work_order",
|
"overproduction_percentage_for_work_order",
|
||||||
|
"job_card_section",
|
||||||
|
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
|
"column_break_24",
|
||||||
|
"job_card_excess_transfer",
|
||||||
"other_settings_section",
|
"other_settings_section",
|
||||||
"update_bom_costs_automatically",
|
"update_bom_costs_automatically",
|
||||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
|
||||||
"column_break_23",
|
"column_break_23",
|
||||||
"make_serial_no_batch_from_work_order"
|
"make_serial_no_batch_from_work_order"
|
||||||
],
|
],
|
||||||
@ -96,10 +99,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Allow multiple material consumptions against a Work Order",
|
"description": "Allow material consumptions without immediately manufacturing finished goods against a Work Order",
|
||||||
"fieldname": "material_consumption",
|
"fieldname": "material_consumption",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Multiple Material Consumption"
|
"label": "Allow Continuous Material Consumption"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@ -175,13 +178,29 @@
|
|||||||
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
|
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Add Corrective Operation Cost in Finished Good Valuation"
|
"label": "Add Corrective Operation Cost in Finished Good Valuation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "job_card_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Job Card"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_24",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Allow transferring raw materials even after the Required Quantity is fulfilled",
|
||||||
|
"fieldname": "job_card_excess_transfer",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Excess Material Transfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-wrench",
|
"icon": "icon-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-16 15:54:38.967341",
|
"modified": "2021-09-13 22:09:09.401559",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@ -814,6 +810,7 @@ def make_wo_order_test_record(**args):
|
|||||||
wo_order.get_items_and_operations_from_bom()
|
wo_order.get_items_and_operations_from_bom()
|
||||||
wo_order.sales_order = args.sales_order or None
|
wo_order.sales_order = args.sales_order or None
|
||||||
wo_order.planned_start_date = args.planned_start_date or now()
|
wo_order.planned_start_date = args.planned_start_date or now()
|
||||||
|
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
||||||
|
|
||||||
if args.source_warehouse:
|
if args.source_warehouse:
|
||||||
for item in wo_order.get("required_items"):
|
for item in wo_order.get("required_items"):
|
||||||
|
@ -1264,9 +1264,9 @@ class StockEntry(StockController):
|
|||||||
po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
|
po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
|
||||||
`tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
|
`tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
|
||||||
|
|
||||||
manufacturing_qty = flt(po_qty.qty)
|
manufacturing_qty = flt(po_qty.qty) or 1
|
||||||
produced_qty = flt(po_qty.produced_qty)
|
produced_qty = flt(po_qty.produced_qty)
|
||||||
trans_qty = flt(po_qty.material_transferred_for_manufacturing)
|
trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1
|
||||||
|
|
||||||
for item in transferred_materials:
|
for item in transferred_materials:
|
||||||
qty= item.qty
|
qty= item.qty
|
||||||
|
Loading…
Reference in New Issue
Block a user