Merge pull request #25221 from Alchez/dev-quality-inspection-accounts

feat: create Quality Inspections from account and stock documents
This commit is contained in:
Marica
2021-06-03 01:39:05 +05:30
committed by GitHub
5 changed files with 292 additions and 69 deletions

View File

@@ -225,7 +225,7 @@ class AccountsController(TransactionBase):
def validate_date_with_fiscal_year(self): def validate_date_with_fiscal_year(self):
if self.meta.get_field("fiscal_year"): if self.meta.get_field("fiscal_year"):
date_field = "" date_field = None
if self.meta.get_field("posting_date"): if self.meta.get_field("posting_date"):
date_field = "posting_date" date_field = "posting_date"
elif self.meta.get_field("transaction_date"): elif self.meta.get_field("transaction_date"):
@@ -1449,6 +1449,7 @@ def validate_and_delete_children(parent, data):
for d in deleted_children: for d in deleted_children:
update_bin_on_delete(d, parent.doctype) update_bin_on_delete(d, parent.doctype)
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'): def check_doc_permissions(doc, perm_type='create'):

View File

@@ -1,17 +1,21 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, 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 json
import frappe, erpnext
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _
import frappe.defaults
from collections import defaultdict 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.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.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 import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate
class QualityInspectionRequiredError(frappe.ValidationError): pass class QualityInspectionRequiredError(frappe.ValidationError): pass
class QualityInspectionRejectedError(frappe.ValidationError): pass class QualityInspectionRejectedError(frappe.ValidationError): pass
@@ -189,7 +193,6 @@ class StockController(AccountsController):
if hasattr(self, "items"): if hasattr(self, "items"):
item_doclist = self.get("items") item_doclist = self.get("items")
elif self.doctype == "Stock Reconciliation": elif self.doctype == "Stock Reconciliation":
import json
item_doclist = [] item_doclist = []
data = json.loads(self.reconciliation_json) data = json.loads(self.reconciliation_json)
for row in data[data.index(self.head_row)+1:]: for row in data[data.index(self.head_row)+1:]:
@@ -319,7 +322,7 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def validate_warehouse(self): 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 warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)])) 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, check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name) 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(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})

View File

@@ -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)) { if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
return; 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"; ? "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) { quality_inspection_field.get_route_options_for_new_doc = function(row) {
if(me.frm.is_new()) return; if(me.frm.is_new()) return;
return { return {
@@ -280,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) { this.frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; let d = locals[cdt][cdn];
return { return {
filters: { filters: {
docstatus: 1, 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(){ get_method_for_payment: function(){
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; 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){ if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){

View File

@@ -1,29 +1,45 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals
import frappe
import unittest import unittest
import frappe
from frappe.utils import nowdate 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.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.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') # test_records = frappe.get_test_records('Quality Inspection')
class TestQualityInspection(unittest.TestCase): class TestQualityInspection(unittest.TestCase):
def setUp(self): def setUp(self):
create_item("_Test Item with QA") 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): 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) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
self.assertRaises(QualityInspectionRequiredError, dn.submit) 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() dn.reload()
self.assertRaises(QualityInspectionRejectedError, dn.submit) self.assertRaises(QualityInspectionRejectedError, dn.submit)
@@ -38,7 +54,9 @@ class TestQualityInspection(unittest.TestCase):
def test_qa_not_submit(self): def test_qa_not_submit(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) 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 dn.items[0].quality_inspection = qa.name
self.assertRaises(QualityInspectionNotSubmittedError, dn.submit) self.assertRaises(QualityInspectionNotSubmittedError, dn.submit)
@@ -48,21 +66,28 @@ class TestQualityInspection(unittest.TestCase):
def test_value_based_qi_readings(self): def test_value_based_qi_readings(self):
# Test QI based on acceptance values (Non formula) # Test QI based on acceptance values (Non formula)
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{ readings = [
"specification": "Iron Content", # numeric reading {
"min_value": 0.1, "specification": "Iron Content", # numeric reading
"max_value": 0.9, "min_value": 0.1,
"reading_1": "0.4" "max_value": 0.9,
}, "reading_1": "0.4"
{ },
"specification": "Particle Inspection Needed", # non-numeric reading {
"numeric": 0, "specification": "Particle Inspection Needed", # non-numeric reading
"value": "Yes", "numeric": 0,
"reading_value": "Yes" "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() qa.save()
# status must be auto set as per formula # status must be auto set as per formula
@@ -74,36 +99,43 @@ class TestQualityInspection(unittest.TestCase):
def test_formula_based_qi_readings(self): def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{ readings = [
"specification": "Iron Content", # numeric reading {
"formula_based_criteria": 1, "specification": "Iron Content", # numeric reading
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", "formula_based_criteria": 1,
"reading_1": "0.4" "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
}, "reading_1": "0.4"
{ },
"specification": "Calcium Content", # numeric reading {
"formula_based_criteria": 1, "specification": "Calcium Content", # numeric reading
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", "formula_based_criteria": 1,
"reading_1": "0.7" "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
}, "reading_1": "0.7"
{ },
"specification": "Mg Content", # numeric reading {
"formula_based_criteria": 1, "specification": "Mg Content", # numeric reading
"acceptance_formula": "mean < 0.9", "formula_based_criteria": 1,
"reading_1": "0.5", "acceptance_formula": "mean < 0.9",
"reading_2": "0.7", "reading_1": "0.5",
"reading_3": "random text" # check if random string input causes issues "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, "specification": "Calcium Content", # non-numeric reading
"numeric": 0, "formula_based_criteria": 1,
"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", "numeric": 0,
"reading_value": "Grade B" "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() qa.save()
# status must be auto set as per formula # status must be auto set as per formula
@@ -115,6 +147,19 @@ class TestQualityInspection(unittest.TestCase):
qa.delete() qa.delete()
dn.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): def create_quality_inspection(**args):
args = frappe._dict(args) args = frappe._dict(args)
qa = frappe.new_doc("Quality Inspection") qa = frappe.new_doc("Quality Inspection")
@@ -134,7 +179,7 @@ def create_quality_inspection(**args):
readings = args.readings readings = args.readings
if args.status == "Rejected": 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): if isinstance(readings, list):
for entry in readings: for entry in readings:
@@ -150,10 +195,11 @@ def create_quality_inspection(**args):
return qa return qa
def create_quality_inspection_parameter(parameter): def create_quality_inspection_parameter(parameter):
if not frappe.db.exists("Quality Inspection Parameter", parameter): if not frappe.db.exists("Quality Inspection Parameter", parameter):
frappe.get_doc({ frappe.get_doc({
"doctype": "Quality Inspection Parameter", "doctype": "Quality Inspection Parameter",
"parameter": parameter, "parameter": parameter,
"description": parameter "description": parameter
}).insert() }).insert()

View File

@@ -115,6 +115,14 @@ frappe.ui.form.on('Stock Entry', {
return; 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"); let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) { quality_inspection_field.get_route_options_for_new_doc = function(row) {
if (frm.is_new()) return; if (frm.is_new()) return;
@@ -155,7 +163,7 @@ frappe.ui.form.on('Stock Entry', {
refresh: function(frm) { refresh: function(frm) {
if(!frm.doc.docstatus) { if(!frm.doc.docstatus) {
frm.trigger('validate_purpose_consumption'); 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() { frappe.model.with_doctype('Material Request', function() {
var mr = frappe.model.get_new_doc('Material Request'); var mr = frappe.model.get_new_doc('Material Request');
var items = frm.get_field('items').grid.get_selected_children(); 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); frappe.set_route('Form', 'Material Request', mr.name);
}); });
}); }, __("Create"));
} }
if(frm.doc.items) { if(frm.doc.items) {