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
commit e7d9200e0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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):
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'):

View File

@ -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']]})

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)) {
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){

View File

@ -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()

View File

@ -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) {