Merge pull request #25221 from Alchez/dev-quality-inspection-accounts
feat: create Quality Inspections from account and stock documents
This commit is contained in:
commit
e7d9200e0f
@ -225,7 +225,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_date_with_fiscal_year(self):
|
||||
if self.meta.get_field("fiscal_year"):
|
||||
date_field = ""
|
||||
date_field = None
|
||||
if self.meta.get_field("posting_date"):
|
||||
date_field = "posting_date"
|
||||
elif self.meta.get_field("transaction_date"):
|
||||
@ -1449,6 +1449,7 @@ def validate_and_delete_children(parent, data):
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
def check_doc_permissions(doc, perm_type='create'):
|
||||
|
@ -1,17 +1,21 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, erpnext
|
||||
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
|
||||
from frappe import _
|
||||
import frappe.defaults
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
|
||||
|
||||
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
||||
class QualityInspectionRejectedError(frappe.ValidationError): pass
|
||||
@ -189,7 +193,6 @@ class StockController(AccountsController):
|
||||
if hasattr(self, "items"):
|
||||
item_doclist = self.get("items")
|
||||
elif self.doctype == "Stock Reconciliation":
|
||||
import json
|
||||
item_doclist = []
|
||||
data = json.loads(self.reconciliation_json)
|
||||
for row in data[data.index(self.head_row)+1:]:
|
||||
@ -319,7 +322,7 @@ class StockController(AccountsController):
|
||||
return serialized_items
|
||||
|
||||
def validate_warehouse(self):
|
||||
from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
|
||||
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
|
||||
|
||||
warehouses = list(set([d.warehouse for d in
|
||||
self.get("items") if getattr(d, "warehouse", None)]))
|
||||
@ -498,6 +501,39 @@ class StockController(AccountsController):
|
||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||
self.company, self.doctype, self.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
inspections = []
|
||||
for item in items:
|
||||
if flt(item.get("sample_size")) > flt(item.get("qty")):
|
||||
frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format(
|
||||
item_name=item.get("item_name"),
|
||||
sample_size=item.get("sample_size"),
|
||||
accepted_quantity=item.get("qty")
|
||||
))
|
||||
|
||||
quality_inspection = frappe.get_doc({
|
||||
"doctype": "Quality Inspection",
|
||||
"inspection_type": "Incoming",
|
||||
"inspected_by": frappe.session.user,
|
||||
"reference_type": doctype,
|
||||
"reference_name": docname,
|
||||
"item_code": item.get("item_code"),
|
||||
"description": item.get("description"),
|
||||
"sample_size": flt(item.get("sample_size")),
|
||||
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||
"batch_no": item.get("batch_no")
|
||||
}).insert()
|
||||
quality_inspection.save()
|
||||
inspections.append(quality_inspection.name)
|
||||
|
||||
return inspections
|
||||
|
||||
|
||||
def is_reposting_pending():
|
||||
return frappe.db.exists("Repost Item Valuation",
|
||||
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||
|
@ -261,11 +261,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
|
||||
return;
|
||||
}
|
||||
var me = this;
|
||||
var inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
|
||||
|
||||
const me = this;
|
||||
if (!this.frm.is_new() && this.frm.doc.docstatus === 0) {
|
||||
this.frm.add_custom_button(__("Quality Inspection(s)"), () => {
|
||||
me.make_quality_inspection();
|
||||
}, __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
|
||||
? "Incoming" : "Outgoing";
|
||||
|
||||
var quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
|
||||
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
|
||||
quality_inspection_field.get_route_options_for_new_doc = function(row) {
|
||||
if(me.frm.is_new()) return;
|
||||
return {
|
||||
@ -280,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
}
|
||||
|
||||
this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
@ -1949,6 +1957,130 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
});
|
||||
},
|
||||
|
||||
make_quality_inspection: function () {
|
||||
let data = [];
|
||||
const fields = [
|
||||
{
|
||||
label: "Items",
|
||||
fieldtype: "Table",
|
||||
fieldname: "items",
|
||||
cannot_add_rows: true,
|
||||
in_place_edit: true,
|
||||
data: data,
|
||||
get_data: () => {
|
||||
return data;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "docname",
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Read Only",
|
||||
fieldname: "item_code",
|
||||
label: __("Item Code"),
|
||||
in_list_view: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Read Only",
|
||||
fieldname: "item_name",
|
||||
label: __("Item Name"),
|
||||
in_list_view: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "qty",
|
||||
label: __("Accepted Quantity"),
|
||||
in_list_view: true,
|
||||
read_only: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "sample_size",
|
||||
label: __("Sample Size"),
|
||||
reqd: true,
|
||||
in_list_view: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "description",
|
||||
label: __("Description"),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "serial_no",
|
||||
label: __("Serial No"),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "batch_no",
|
||||
label: __("Batch No"),
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const me = this;
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Items for Quality Inspection"),
|
||||
fields: fields,
|
||||
primary_action: function () {
|
||||
const data = dialog.get_values();
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
docname: me.frm.doc.name,
|
||||
items: data.items
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (r.message.length > 0) {
|
||||
if (r.message.length === 1) {
|
||||
frappe.set_route("Form", "Quality Inspection", r.message[0]);
|
||||
} else {
|
||||
frappe.route_options = {
|
||||
"reference_type": me.frm.doc.doctype,
|
||||
"reference_name": me.frm.doc.name
|
||||
};
|
||||
frappe.set_route("List", "Quality Inspection");
|
||||
}
|
||||
}
|
||||
dialog.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __("Create")
|
||||
});
|
||||
|
||||
this.frm.doc.items.forEach(item => {
|
||||
if (!item.quality_inspection) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
"docname": item.name,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
data = dialog.fields_dict.items.df.data;
|
||||
if (!data.length) {
|
||||
frappe.msgprint(__("All items in this document already have a linked Quality Inspection."));
|
||||
} else {
|
||||
dialog.show();
|
||||
}
|
||||
},
|
||||
|
||||
get_method_for_payment: function(){
|
||||
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
|
||||
if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){
|
||||
|
@ -1,29 +1,45 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
from erpnext.controllers.stock_controller import (
|
||||
QualityInspectionNotSubmittedError,
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
make_quality_inspections,
|
||||
)
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError
|
||||
|
||||
# test_records = frappe.get_test_records('Quality Inspection')
|
||||
|
||||
|
||||
class TestQualityInspection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_item("_Test Item with QA")
|
||||
frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1)
|
||||
frappe.db.set_value(
|
||||
"Item", "_Test Item with QA", "inspection_required_before_delivery", 1
|
||||
)
|
||||
|
||||
def test_qa_for_delivery(self):
|
||||
make_stock_entry(item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item with QA",
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=1,
|
||||
basic_rate=100
|
||||
)
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
|
||||
self.assertRaises(QualityInspectionRequiredError, dn.submit)
|
||||
|
||||
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected")
|
||||
qa = create_quality_inspection(
|
||||
reference_type="Delivery Note", reference_name=dn.name, status="Rejected"
|
||||
)
|
||||
dn.reload()
|
||||
self.assertRaises(QualityInspectionRejectedError, dn.submit)
|
||||
|
||||
@ -38,7 +54,9 @@ class TestQualityInspection(unittest.TestCase):
|
||||
|
||||
def test_qa_not_submit(self):
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True)
|
||||
qa = create_quality_inspection(
|
||||
reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True
|
||||
)
|
||||
dn.items[0].quality_inspection = qa.name
|
||||
self.assertRaises(QualityInspectionNotSubmittedError, dn.submit)
|
||||
|
||||
@ -48,21 +66,28 @@ class TestQualityInspection(unittest.TestCase):
|
||||
def test_value_based_qi_readings(self):
|
||||
# Test QI based on acceptance values (Non formula)
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
readings = [{
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"min_value": 0.1,
|
||||
"max_value": 0.9,
|
||||
"reading_1": "0.4"
|
||||
},
|
||||
{
|
||||
"specification": "Particle Inspection Needed", # non-numeric reading
|
||||
"numeric": 0,
|
||||
"value": "Yes",
|
||||
"reading_value": "Yes"
|
||||
}]
|
||||
readings = [
|
||||
{
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"min_value": 0.1,
|
||||
"max_value": 0.9,
|
||||
"reading_1": "0.4"
|
||||
},
|
||||
{
|
||||
"specification": "Particle Inspection Needed", # non-numeric reading
|
||||
"numeric": 0,
|
||||
"value": "Yes",
|
||||
"reading_value": "Yes"
|
||||
}
|
||||
]
|
||||
|
||||
qa = create_quality_inspection(
|
||||
reference_type="Delivery Note",
|
||||
reference_name=dn.name,
|
||||
readings=readings,
|
||||
do_not_save=True
|
||||
)
|
||||
|
||||
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
|
||||
readings=readings, do_not_save=True)
|
||||
qa.save()
|
||||
|
||||
# status must be auto set as per formula
|
||||
@ -74,36 +99,43 @@ class TestQualityInspection(unittest.TestCase):
|
||||
|
||||
def test_formula_based_qi_readings(self):
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
readings = [{
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
|
||||
"reading_1": "0.4"
|
||||
},
|
||||
{
|
||||
"specification": "Calcium Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
|
||||
"reading_1": "0.7"
|
||||
},
|
||||
{
|
||||
"specification": "Mg Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "mean < 0.9",
|
||||
"reading_1": "0.5",
|
||||
"reading_2": "0.7",
|
||||
"reading_3": "random text" # check if random string input causes issues
|
||||
},
|
||||
{
|
||||
"specification": "Calcium Content", # non-numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"numeric": 0,
|
||||
"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
|
||||
"reading_value": "Grade B"
|
||||
}]
|
||||
readings = [
|
||||
{
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
|
||||
"reading_1": "0.4"
|
||||
},
|
||||
{
|
||||
"specification": "Calcium Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
|
||||
"reading_1": "0.7"
|
||||
},
|
||||
{
|
||||
"specification": "Mg Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "mean < 0.9",
|
||||
"reading_1": "0.5",
|
||||
"reading_2": "0.7",
|
||||
"reading_3": "random text" # check if random string input causes issues
|
||||
},
|
||||
{
|
||||
"specification": "Calcium Content", # non-numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"numeric": 0,
|
||||
"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
|
||||
"reading_value": "Grade B"
|
||||
}
|
||||
]
|
||||
|
||||
qa = create_quality_inspection(
|
||||
reference_type="Delivery Note",
|
||||
reference_name=dn.name,
|
||||
readings=readings,
|
||||
do_not_save=True
|
||||
)
|
||||
|
||||
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
|
||||
readings=readings, do_not_save=True)
|
||||
qa.save()
|
||||
|
||||
# status must be auto set as per formula
|
||||
@ -115,6 +147,19 @@ class TestQualityInspection(unittest.TestCase):
|
||||
qa.delete()
|
||||
dn.delete()
|
||||
|
||||
def test_make_quality_inspections_from_linked_document(self):
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
for item in dn.items:
|
||||
item.sample_size = item.qty
|
||||
quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items)
|
||||
self.assertEqual(len(dn.items), len(quality_inspections))
|
||||
|
||||
# cleanup
|
||||
for qi in quality_inspections:
|
||||
frappe.delete_doc("Quality Inspection", qi)
|
||||
dn.delete()
|
||||
|
||||
|
||||
def create_quality_inspection(**args):
|
||||
args = frappe._dict(args)
|
||||
qa = frappe.new_doc("Quality Inspection")
|
||||
@ -134,7 +179,7 @@ def create_quality_inspection(**args):
|
||||
readings = args.readings
|
||||
|
||||
if args.status == "Rejected":
|
||||
readings["reading_1"] = "12" # status is auto set in child on save
|
||||
readings["reading_1"] = "12" # status is auto set in child on save
|
||||
|
||||
if isinstance(readings, list):
|
||||
for entry in readings:
|
||||
@ -150,10 +195,11 @@ def create_quality_inspection(**args):
|
||||
|
||||
return qa
|
||||
|
||||
|
||||
def create_quality_inspection_parameter(parameter):
|
||||
if not frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
frappe.get_doc({
|
||||
"doctype": "Quality Inspection Parameter",
|
||||
"parameter": parameter,
|
||||
"description": parameter
|
||||
}).insert()
|
||||
}).insert()
|
||||
|
@ -115,6 +115,14 @@ frappe.ui.form.on('Stock Entry', {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frm.is_new() && frm.doc.docstatus === 0) {
|
||||
frm.add_custom_button(__("Quality Inspection(s)"), () => {
|
||||
let transaction_controller = new erpnext.TransactionController({ frm: frm });
|
||||
transaction_controller.make_quality_inspection();
|
||||
}, __("Create"));
|
||||
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
|
||||
quality_inspection_field.get_route_options_for_new_doc = function(row) {
|
||||
if (frm.is_new()) return;
|
||||
@ -155,7 +163,7 @@ frappe.ui.form.on('Stock Entry', {
|
||||
refresh: function(frm) {
|
||||
if(!frm.doc.docstatus) {
|
||||
frm.trigger('validate_purpose_consumption');
|
||||
frm.add_custom_button(__('Create Material Request'), function() {
|
||||
frm.add_custom_button(__('Material Request'), function() {
|
||||
frappe.model.with_doctype('Material Request', function() {
|
||||
var mr = frappe.model.get_new_doc('Material Request');
|
||||
var items = frm.get_field('items').grid.get_selected_children();
|
||||
@ -178,7 +186,7 @@ frappe.ui.form.on('Stock Entry', {
|
||||
});
|
||||
frappe.set_route('Form', 'Material Request', mr.name);
|
||||
});
|
||||
});
|
||||
}, __("Create"));
|
||||
}
|
||||
|
||||
if(frm.doc.items) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user