Merge branch 'develop' into partially-submit-drop-ship-items-issue
This commit is contained in:
commit
cffc489d57
@ -75,62 +75,70 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
return Object.assign(options, {
|
||||
checkboxColumn: true,
|
||||
events: {
|
||||
onCheckRow: function(data) {
|
||||
onCheckRow: function (data) {
|
||||
if (!data) return;
|
||||
|
||||
const data_doctype = $(
|
||||
data[2].html
|
||||
)[0].attributes.getNamedItem("data-doctype").value;
|
||||
const tree_type = frappe.query_report.filters[0].value;
|
||||
if (data_doctype != tree_type) return;
|
||||
|
||||
row_name = data[2].content;
|
||||
length = data.length;
|
||||
|
||||
var tree_type = frappe.query_report.filters[0].value;
|
||||
|
||||
if(tree_type == "Supplier" || tree_type == "Item") {
|
||||
row_values = data.slice(4,length-1).map(function (column) {
|
||||
return column.content;
|
||||
})
|
||||
}
|
||||
else {
|
||||
row_values = data.slice(3,length-1).map(function (column) {
|
||||
return column.content;
|
||||
})
|
||||
if (tree_type == "Supplier") {
|
||||
row_values = data
|
||||
.slice(4, length - 1)
|
||||
.map(function (column) {
|
||||
return column.content;
|
||||
});
|
||||
} else if (tree_type == "Item") {
|
||||
row_values = data
|
||||
.slice(5, length - 1)
|
||||
.map(function (column) {
|
||||
return column.content;
|
||||
});
|
||||
} else {
|
||||
row_values = data
|
||||
.slice(3, length - 1)
|
||||
.map(function (column) {
|
||||
return column.content;
|
||||
});
|
||||
}
|
||||
|
||||
entry = {
|
||||
'name':row_name,
|
||||
'values':row_values
|
||||
}
|
||||
entry = {
|
||||
name: row_name,
|
||||
values: row_values,
|
||||
};
|
||||
|
||||
let raw_data = frappe.query_report.chart.data;
|
||||
let new_datasets = raw_data.datasets;
|
||||
|
||||
var found = false;
|
||||
|
||||
for(var i=0; i < new_datasets.length;i++){
|
||||
if(new_datasets[i].name == row_name){
|
||||
found = true;
|
||||
new_datasets.splice(i,1);
|
||||
break;
|
||||
let element_found = new_datasets.some((element, index, array)=>{
|
||||
if(element.name == row_name){
|
||||
array.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if(!found){
|
||||
if (!element_found) {
|
||||
new_datasets.push(entry);
|
||||
}
|
||||
|
||||
let new_data = {
|
||||
labels: raw_data.labels,
|
||||
datasets: new_datasets
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
frappe.query_report.chart.update(new_data)
|
||||
},500)
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
frappe.query_report.chart.draw(true);
|
||||
}, 1000)
|
||||
datasets: new_datasets,
|
||||
};
|
||||
chart_options = {
|
||||
data: new_data,
|
||||
type: "line",
|
||||
};
|
||||
frappe.query_report.render_chart(chart_options);
|
||||
|
||||
frappe.query_report.raw_chart_data = new_data;
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -328,6 +328,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
target_doc.po_detail = source_doc.po_detail
|
||||
target_doc.pr_detail = source_doc.pr_detail
|
||||
target_doc.purchase_invoice_item = source_doc.name
|
||||
target_doc.price_list_rate = 0
|
||||
|
||||
elif doctype == "Delivery Note":
|
||||
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||
@ -353,6 +354,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
target_doc.dn_detail = source_doc.dn_detail
|
||||
target_doc.expense_account = source_doc.expense_account
|
||||
target_doc.sales_invoice_item = source_doc.name
|
||||
target_doc.price_list_rate = 0
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
|
@ -233,7 +233,7 @@ class SellingController(StockController):
|
||||
'allow_zero_valuation': d.allow_zero_valuation_rate,
|
||||
'sales_invoice_item': d.get("sales_invoice_item"),
|
||||
'dn_detail': d.get("dn_detail"),
|
||||
'incoming_rate': p.incoming_rate
|
||||
'incoming_rate': p.get("incoming_rate")
|
||||
}))
|
||||
else:
|
||||
il.append(frappe._dict({
|
||||
@ -252,7 +252,7 @@ class SellingController(StockController):
|
||||
'allow_zero_valuation': d.allow_zero_valuation_rate,
|
||||
'sales_invoice_item': d.get("sales_invoice_item"),
|
||||
'dn_detail': d.get("dn_detail"),
|
||||
'incoming_rate': d.incoming_rate
|
||||
'incoming_rate': d.get("incoming_rate")
|
||||
}))
|
||||
return il
|
||||
|
||||
|
@ -6,6 +6,7 @@ import unittest
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
|
||||
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter
|
||||
|
||||
from six import string_types
|
||||
|
||||
@ -56,6 +57,8 @@ def make_quality_inspection_template():
|
||||
|
||||
qc = frappe.new_doc("Quality Inspection Template")
|
||||
qc.quality_inspection_template_name = qc_template
|
||||
|
||||
create_quality_inspection_parameter("Moisture")
|
||||
qc.append('item_quality_inspection_parameter', {
|
||||
"specification": "Moisture",
|
||||
"value": "< 5%",
|
||||
|
@ -17,6 +17,8 @@
|
||||
"enable_free_follow_ups",
|
||||
"max_visits",
|
||||
"valid_days",
|
||||
"inpatient_settings_section",
|
||||
"allow_discharge_despite_unbilled_services",
|
||||
"healthcare_service_items",
|
||||
"inpatient_visit_charge_item",
|
||||
"op_consulting_charge_item",
|
||||
@ -302,11 +304,22 @@
|
||||
"fieldname": "enable_free_follow_ups",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Free Follow-ups"
|
||||
},
|
||||
{
|
||||
"fieldname": "inpatient_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Inpatient Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_discharge_despite_unbilled_services",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Discharge Despite Unbilled Healthcare Services"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-08 15:17:21.543218",
|
||||
"modified": "2021-01-04 10:19:22.329272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Healthcare Settings",
|
||||
|
@ -5,7 +5,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe, json
|
||||
from frappe import _
|
||||
from frappe.utils import today, now_datetime, getdate, get_datetime
|
||||
from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
|
||||
from frappe.model.document import Document
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
@ -113,6 +113,7 @@ def schedule_inpatient(args):
|
||||
inpatient_record.status = 'Admission Scheduled'
|
||||
inpatient_record.save(ignore_permissions = True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def schedule_discharge(args):
|
||||
discharge_order = json.loads(args)
|
||||
@ -126,16 +127,19 @@ def schedule_discharge(args):
|
||||
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
|
||||
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
|
||||
|
||||
|
||||
def set_details_from_ip_order(inpatient_record, ip_order):
|
||||
for key in ip_order:
|
||||
inpatient_record.set(key, ip_order[key])
|
||||
|
||||
|
||||
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
|
||||
for item in encounter_child:
|
||||
table = inpatient_record.append(inpatient_record_child)
|
||||
for df in table.meta.get('fields'):
|
||||
table.set(df.fieldname, item.get(df.fieldname))
|
||||
|
||||
|
||||
def check_out_inpatient(inpatient_record):
|
||||
if inpatient_record.inpatient_occupancies:
|
||||
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
|
||||
@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record):
|
||||
inpatient_occupancy.check_out = now_datetime()
|
||||
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
|
||||
|
||||
|
||||
def discharge_patient(inpatient_record):
|
||||
validate_invoiced_inpatient(inpatient_record)
|
||||
validate_inpatient_invoicing(inpatient_record)
|
||||
inpatient_record.discharge_date = today()
|
||||
inpatient_record.status = "Discharged"
|
||||
|
||||
inpatient_record.save(ignore_permissions = True)
|
||||
|
||||
def validate_invoiced_inpatient(inpatient_record):
|
||||
pending_invoices = []
|
||||
|
||||
def validate_inpatient_invoicing(inpatient_record):
|
||||
if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
|
||||
return
|
||||
|
||||
pending_invoices = get_pending_invoices(inpatient_record)
|
||||
|
||||
if pending_invoices:
|
||||
message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
|
||||
|
||||
formatted_doc_rows = ''
|
||||
|
||||
for doctype, docnames in pending_invoices.items():
|
||||
formatted_doc_rows += """
|
||||
<td>{0}</td>
|
||||
<td>{1}</td>
|
||||
</tr>""".format(doctype, docnames)
|
||||
|
||||
message += """
|
||||
<table class='table'>
|
||||
<thead>
|
||||
<th>{0}</th>
|
||||
<th>{1}</th>
|
||||
</thead>
|
||||
{2}
|
||||
</table>
|
||||
""".format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
|
||||
|
||||
frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
|
||||
|
||||
|
||||
def get_pending_invoices(inpatient_record):
|
||||
pending_invoices = {}
|
||||
if inpatient_record.inpatient_occupancies:
|
||||
service_unit_names = False
|
||||
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
|
||||
if inpatient_occupancy.invoiced != 1:
|
||||
if not inpatient_occupancy.invoiced:
|
||||
if service_unit_names:
|
||||
service_unit_names += ", " + inpatient_occupancy.service_unit
|
||||
else:
|
||||
service_unit_names = inpatient_occupancy.service_unit
|
||||
if service_unit_names:
|
||||
pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")")
|
||||
pending_invoices["Inpatient Occupancy"] = service_unit_names
|
||||
|
||||
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
|
||||
|
||||
for doc in docs:
|
||||
doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record)
|
||||
doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
|
||||
if doc_name_list:
|
||||
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
|
||||
|
||||
if pending_invoices:
|
||||
frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
|
||||
.join(pending_invoices)), title=_('Unbilled Invoices'))
|
||||
return pending_invoices
|
||||
|
||||
|
||||
def get_pending_doc(doc, doc_name_list, pending_invoices):
|
||||
if doc_name_list:
|
||||
doc_ids = False
|
||||
for doc_name in doc_name_list:
|
||||
doc_link = get_link_to_form(doc, doc_name.name)
|
||||
if doc_ids:
|
||||
doc_ids += ", "+doc_name.name
|
||||
doc_ids += ", " + doc_link
|
||||
else:
|
||||
doc_ids = doc_name.name
|
||||
doc_ids = doc_link
|
||||
if doc_ids:
|
||||
pending_invoices.append(doc + " (" + doc_ids + ")")
|
||||
pending_invoices[doc] = doc_ids
|
||||
|
||||
return pending_invoices
|
||||
|
||||
def get_inpatient_docs_not_invoiced(doc, inpatient_record):
|
||||
|
||||
def get_unbilled_inpatient_docs(doc, inpatient_record):
|
||||
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
|
||||
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
|
||||
|
||||
|
||||
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
|
||||
inpatient_record.admitted_datetime = check_in
|
||||
inpatient_record.status = 'Admitted'
|
||||
@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N
|
||||
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
|
||||
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
|
||||
|
||||
|
||||
def transfer_patient(inpatient_record, service_unit, check_in):
|
||||
item_line = inpatient_record.append('inpatient_occupancies', {})
|
||||
item_line.service_unit = service_unit
|
||||
@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in):
|
||||
|
||||
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
|
||||
|
||||
|
||||
def patient_leave_service_unit(inpatient_record, check_out, leave_from):
|
||||
if inpatient_record.inpatient_occupancies:
|
||||
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
|
||||
@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from):
|
||||
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
|
||||
inpatient_record.save(ignore_permissions = True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_leave_from(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -40,6 +40,31 @@ class TestInpatientRecord(unittest.TestCase):
|
||||
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
|
||||
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
|
||||
|
||||
def test_allow_discharge_despite_unbilled_services(self):
|
||||
frappe.db.sql("""delete from `tabInpatient Record`""")
|
||||
setup_inpatient_settings()
|
||||
patient = create_patient()
|
||||
# Schedule Admission
|
||||
ip_record = create_inpatient(patient)
|
||||
ip_record.expected_length_of_stay = 0
|
||||
ip_record.save(ignore_permissions = True)
|
||||
|
||||
# Admit
|
||||
service_unit = get_healthcare_service_unit()
|
||||
admit_patient(ip_record, service_unit, now_datetime())
|
||||
|
||||
# Discharge
|
||||
schedule_discharge(frappe.as_json({"patient": patient}))
|
||||
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
|
||||
|
||||
ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
|
||||
# Should not validate Pending Invoices
|
||||
ip_record.discharge()
|
||||
|
||||
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
|
||||
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
|
||||
|
||||
|
||||
def test_validate_overlap_admission(self):
|
||||
frappe.db.sql("""delete from `tabInpatient Record`""")
|
||||
patient = create_patient()
|
||||
@ -63,6 +88,13 @@ def mark_invoiced_inpatient_occupancy(ip_record):
|
||||
inpatient_occupancy.invoiced = 1
|
||||
ip_record.save(ignore_permissions = True)
|
||||
|
||||
|
||||
def setup_inpatient_settings():
|
||||
settings = frappe.get_single("Healthcare Settings")
|
||||
settings.allow_discharge_despite_unbilled_services = 1
|
||||
settings.save()
|
||||
|
||||
|
||||
def create_inpatient(patient):
|
||||
patient_obj = frappe.get_doc('Patient', patient)
|
||||
inpatient_record = frappe.new_doc('Inpatient Record')
|
||||
@ -78,6 +110,7 @@ def create_inpatient(patient):
|
||||
inpatient_record.scheduled_date = today()
|
||||
return inpatient_record
|
||||
|
||||
|
||||
def get_healthcare_service_unit():
|
||||
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
|
||||
if not service_unit:
|
||||
@ -105,6 +138,7 @@ def get_healthcare_service_unit():
|
||||
return service_unit.name
|
||||
return service_unit
|
||||
|
||||
|
||||
def get_service_unit_type():
|
||||
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1})
|
||||
|
||||
@ -116,6 +150,7 @@ def get_service_unit_type():
|
||||
return service_unit_type.name
|
||||
return service_unit_type
|
||||
|
||||
|
||||
def create_patient():
|
||||
patient = frappe.db.exists('Patient', '_Test IPD Patient')
|
||||
if not patient:
|
||||
|
@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase):
|
||||
self.assertEquals(appointment.status, 'Open')
|
||||
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
|
||||
self.assertEquals(appointment.status, 'Scheduled')
|
||||
create_encounter(appointment)
|
||||
encounter = create_encounter(appointment)
|
||||
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
|
||||
encounter.cancel()
|
||||
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
|
||||
|
||||
def test_start_encounter(self):
|
||||
patient, medical_department, practitioner = create_healthcare_docs()
|
||||
|
@ -5,10 +5,10 @@ from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import getdate, flt
|
||||
from frappe.utils import getdate, flt, nowdate
|
||||
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
|
||||
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
|
||||
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient
|
||||
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
|
||||
|
||||
class TestTherapyPlan(unittest.TestCase):
|
||||
def test_creation_on_encounter_submission(self):
|
||||
@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
frappe.get_doc(session).submit()
|
||||
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
|
||||
|
||||
patient, medical_department, practitioner = create_healthcare_docs()
|
||||
appointment = create_appointment(patient, practitioner, nowdate())
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
|
||||
session = frappe.get_doc(session)
|
||||
session.submit()
|
||||
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
|
||||
session.cancel()
|
||||
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
|
||||
|
||||
def test_therapy_plan_from_template(self):
|
||||
patient = create_patient()
|
||||
template = create_therapy_plan_template()
|
||||
|
@ -47,7 +47,7 @@ class TherapyPlan(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_therapy_session(therapy_plan, patient, therapy_type, company):
|
||||
def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None):
|
||||
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
|
||||
|
||||
therapy_session = frappe.new_doc('Therapy Session')
|
||||
@ -58,6 +58,7 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company):
|
||||
therapy_session.duration = therapy_type.default_duration
|
||||
therapy_session.rate = therapy_type.rate
|
||||
therapy_session.exercises = therapy_type.exercises
|
||||
therapy_session.appointment = appointment
|
||||
|
||||
if frappe.flags.in_test:
|
||||
therapy_session.start_date = today()
|
||||
|
@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('appointment', function() {
|
||||
|
||||
return {
|
||||
filters: {
|
||||
'status': ['in', ['Open', 'Scheduled']]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
@ -43,7 +43,14 @@ class TherapySession(Document):
|
||||
self.update_sessions_count_in_therapy_plan()
|
||||
insert_session_medical_record(self)
|
||||
|
||||
def on_update(self):
|
||||
if self.appointment:
|
||||
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
|
||||
|
||||
def on_cancel(self):
|
||||
if self.appointment:
|
||||
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
|
||||
|
||||
self.update_sessions_count_in_therapy_plan(on_cancel=True)
|
||||
|
||||
def update_sessions_count_in_therapy_plan(self, on_cancel=False):
|
||||
|
@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2020-10-16 15:02:04.283657",
|
||||
"modified": "2021-01-01 16:54:33.477439",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
@ -855,7 +855,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "employee_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
@ -11,6 +11,7 @@
|
||||
"employee",
|
||||
"employee_name",
|
||||
"department",
|
||||
"company",
|
||||
"column_break1",
|
||||
"leave_type",
|
||||
"from_date",
|
||||
@ -219,6 +220,15 @@
|
||||
"label": "Leave Policy Assignment",
|
||||
"options": "Leave Policy Assignment",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-ok",
|
||||
@ -226,7 +236,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-20 14:25:10.314323",
|
||||
"modified": "2021-01-04 18:46:13.184104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Allocation",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2019-05-09 15:47:39.760406",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
@ -8,6 +9,7 @@
|
||||
"leave_type",
|
||||
"transaction_type",
|
||||
"transaction_name",
|
||||
"company",
|
||||
"leaves",
|
||||
"column_break_7",
|
||||
"from_date",
|
||||
@ -106,12 +108,22 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Holiday List",
|
||||
"options": "Holiday List"
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-09-04 12:16:36.569066",
|
||||
"links": [],
|
||||
"modified": "2021-01-04 18:47:45.146652",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Ledger Entry",
|
||||
|
@ -362,6 +362,27 @@ class TestLoan(unittest.TestCase):
|
||||
unpledge_request.load_from_db()
|
||||
self.assertEqual(unpledge_request.docstatus, 1)
|
||||
|
||||
def test_santined_loan_security_unpledge(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
"qty": 4000.00
|
||||
}]
|
||||
|
||||
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
|
||||
create_pledge(loan_application)
|
||||
|
||||
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
|
||||
loan.submit()
|
||||
|
||||
self.assertEquals(loan.loan_amount, 1000000)
|
||||
|
||||
unpledge_map = {'Test Security 1': 4000}
|
||||
unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
|
||||
unpledge_request.submit()
|
||||
unpledge_request.status = 'Approved'
|
||||
unpledge_request.save()
|
||||
unpledge_request.submit()
|
||||
|
||||
def test_disbursal_check_with_shortfall(self):
|
||||
pledges = [{
|
||||
"loan_security": "Test Security 2",
|
||||
|
@ -44,10 +44,16 @@ class LoanSecurityUnpledge(Document):
|
||||
"valid_upto": (">=", get_datetime())
|
||||
}, as_list=1))
|
||||
|
||||
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
|
||||
'total_interest_payable', 'written_off_amount'])
|
||||
loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
|
||||
'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
|
||||
|
||||
if loan_details.status == 'Disbursed':
|
||||
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
|
||||
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
|
||||
else:
|
||||
pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
|
||||
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
|
||||
|
||||
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
|
||||
security_value = 0
|
||||
unpledge_qty_map = {}
|
||||
ltv_ratio = 0
|
||||
|
@ -742,3 +742,5 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
||||
erpnext.patches.v13_0.add_po_to_global_search
|
||||
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
|
||||
erpnext.patches.v13_0.set_company_in_leave_ledger_entry
|
||||
erpnext.patches.v13_0.convert_qi_parameter_to_link_field
|
||||
|
23
erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
Normal file
23
erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter')
|
||||
|
||||
# get all distinct parameters from QI readigs table
|
||||
reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"])
|
||||
reading_params = [d.specification for d in reading_params]
|
||||
|
||||
# get all distinct parameters from QI Template as some may be unused in QI
|
||||
template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"])
|
||||
template_params = [d.specification for d in template_params]
|
||||
|
||||
params = list(set(reading_params + template_params))
|
||||
|
||||
for parameter in params:
|
||||
if not frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
frappe.get_doc({
|
||||
"doctype": "Quality Inspection Parameter",
|
||||
"parameter": parameter,
|
||||
"description": parameter
|
||||
}).insert(ignore_permissions=True)
|
@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('HR', 'doctype', 'Leave Allocation')
|
||||
frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry')
|
||||
frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""")
|
||||
frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""")
|
@ -5,11 +5,11 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
# udpate sales cycle
|
||||
# update sales cycle
|
||||
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
|
||||
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d)
|
||||
|
||||
# udpate purchase cycle
|
||||
# update purchase cycle
|
||||
for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']:
|
||||
frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)
|
||||
|
||||
|
@ -46,7 +46,7 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
}
|
||||
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
|
||||
}
|
||||
if ((frm.doc.employees || []).length) {
|
||||
if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.page.set_primary_action(__('Create Salary Slips'), () => {
|
||||
frm.save('Submit').then(() => {
|
||||
|
@ -21,6 +21,9 @@ class PayrollEntry(Document):
|
||||
if cint(entries) == len(self.employees):
|
||||
self.set_onload("submitted_ss", True)
|
||||
|
||||
def validate(self):
|
||||
self.number_of_employees = len(self.employees)
|
||||
|
||||
def on_submit(self):
|
||||
self.create_salary_slips()
|
||||
|
||||
@ -113,7 +116,7 @@ class PayrollEntry(Document):
|
||||
for d in employees:
|
||||
self.append('employees', d)
|
||||
|
||||
self.number_of_employees = len(employees)
|
||||
self.number_of_employees = len(self.employees)
|
||||
if self.validate_attendance:
|
||||
return self.validate_employee_attendance()
|
||||
|
||||
@ -145,8 +148,8 @@ class PayrollEntry(Document):
|
||||
"""
|
||||
self.check_permission('write')
|
||||
self.created = 1
|
||||
emp_list = [d.employee for d in self.get_emp_list()]
|
||||
if emp_list:
|
||||
employees = [emp.employee for emp in self.employees]
|
||||
if employees:
|
||||
args = frappe._dict({
|
||||
"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
|
||||
"payroll_frequency": self.payroll_frequency,
|
||||
@ -160,10 +163,10 @@ class PayrollEntry(Document):
|
||||
"exchange_rate": self.exchange_rate,
|
||||
"currency": self.currency
|
||||
})
|
||||
if len(emp_list) > 30:
|
||||
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args)
|
||||
if len(employees) > 30:
|
||||
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
|
||||
else:
|
||||
create_salary_slips_for_employees(emp_list, args, publish_progress=False)
|
||||
create_salary_slips_for_employees(employees, args, publish_progress=False)
|
||||
# since this method is called via frm.call this doc needs to be updated manually
|
||||
self.reload()
|
||||
|
||||
|
@ -151,7 +151,6 @@ frappe.ui.form.on("Salary Slip", {
|
||||
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
|
||||
frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
|
||||
frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
|
||||
calculate_totals(frm);
|
||||
frm.trigger("set_dynamic_labels");
|
||||
},
|
||||
|
||||
|
@ -143,8 +143,8 @@ class SalarySlip(TransactionBase):
|
||||
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
|
||||
self.set_time_sheet()
|
||||
self.pull_sal_struct()
|
||||
payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"])
|
||||
return [payroll_based_on, consider_unmarked_attendance_as]
|
||||
ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
|
||||
return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
|
||||
|
||||
def set_time_sheet(self):
|
||||
if self.salary_slip_based_on_timesheet:
|
||||
@ -424,16 +424,19 @@ class SalarySlip(TransactionBase):
|
||||
def calculate_net_pay(self):
|
||||
if self.salary_structure:
|
||||
self.calculate_component_amounts("earnings")
|
||||
self.gross_pay = self.get_component_totals("earnings")
|
||||
self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
|
||||
self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
|
||||
|
||||
if self.salary_structure:
|
||||
self.calculate_component_amounts("deductions")
|
||||
self.total_deduction = self.get_component_totals("deductions")
|
||||
self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
|
||||
|
||||
self.set_loan_repayment()
|
||||
self.set_component_amounts_based_on_payment_days()
|
||||
self.set_net_pay()
|
||||
|
||||
def set_net_pay(self):
|
||||
self.total_deduction = self.get_component_totals("deductions")
|
||||
self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
|
||||
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
|
||||
self.rounded_total = rounded(self.net_pay)
|
||||
self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay'))
|
||||
@ -455,8 +458,6 @@ class SalarySlip(TransactionBase):
|
||||
else:
|
||||
self.add_tax_components(payroll_period)
|
||||
|
||||
self.set_component_amounts_based_on_payment_days(component_type)
|
||||
|
||||
def add_structure_components(self, component_type):
|
||||
data = self.get_data_for_eval()
|
||||
for struct_row in self._salary_structure_doc.get(component_type):
|
||||
@ -576,7 +577,7 @@ class SalarySlip(TransactionBase):
|
||||
'default_amount': amount if not struct_row.get("is_additional_component") else 0,
|
||||
'depends_on_payment_days' : struct_row.depends_on_payment_days,
|
||||
'salary_component' : struct_row.salary_component,
|
||||
'abbr' : struct_row.abbr,
|
||||
'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
|
||||
'additional_salary': additional_salary,
|
||||
'do_not_include_in_total' : struct_row.do_not_include_in_total,
|
||||
'is_tax_applicable': struct_row.is_tax_applicable,
|
||||
@ -813,7 +814,7 @@ class SalarySlip(TransactionBase):
|
||||
cint(row.depends_on_payment_days) and cint(self.total_working_days) and
|
||||
(not self.salary_slip_based_on_timesheet or
|
||||
getdate(self.start_date) < joining_date or
|
||||
getdate(self.end_date) > relieving_date
|
||||
(relieving_date and getdate(self.end_date) > relieving_date)
|
||||
)):
|
||||
additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
|
||||
/ cint(self.total_working_days)), row.precision("additional_amount"))
|
||||
@ -946,15 +947,21 @@ class SalarySlip(TransactionBase):
|
||||
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
|
||||
return struct_row
|
||||
|
||||
def get_component_totals(self, component_type):
|
||||
def get_component_totals(self, component_type, depends_on_payment_days=0):
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
|
||||
total = 0.0
|
||||
for d in self.get(component_type):
|
||||
if not d.do_not_include_in_total:
|
||||
d.amount = flt(d.amount, d.precision("amount"))
|
||||
total += d.amount
|
||||
if depends_on_payment_days:
|
||||
amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
|
||||
else:
|
||||
amount = flt(d.amount, d.precision("amount"))
|
||||
total += amount
|
||||
return total
|
||||
|
||||
def set_component_amounts_based_on_payment_days(self, component_type):
|
||||
def set_component_amounts_based_on_payment_days(self):
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
|
||||
@ -964,8 +971,9 @@ class SalarySlip(TransactionBase):
|
||||
if not joining_date:
|
||||
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
|
||||
|
||||
for d in self.get(component_type):
|
||||
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
|
||||
for component_type in ("earnings", "deductions"):
|
||||
for d in self.get(component_type):
|
||||
d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
|
||||
|
||||
def set_loan_repayment(self):
|
||||
self.total_loan_repayment = 0
|
||||
@ -1089,17 +1097,17 @@ class SalarySlip(TransactionBase):
|
||||
self.calculate_net_pay()
|
||||
|
||||
def set_totals(self):
|
||||
self.gross_pay = 0
|
||||
self.gross_pay = 0.0
|
||||
if self.salary_slip_based_on_timesheet == 1:
|
||||
self.calculate_total_for_salary_slip_based_on_timesheet()
|
||||
else:
|
||||
self.total_deduction = 0
|
||||
self.total_deduction = 0.0
|
||||
if self.earnings:
|
||||
for earning in self.earnings:
|
||||
self.gross_pay += flt(earning.amount)
|
||||
self.gross_pay += flt(earning.amount, earning.precision("amount"))
|
||||
if self.deductions:
|
||||
for deduction in self.deductions:
|
||||
self.total_deduction += flt(deduction.amount)
|
||||
self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
|
||||
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
|
||||
self.set_base_totals()
|
||||
|
||||
@ -1145,8 +1153,10 @@ class SalarySlip(TransactionBase):
|
||||
fields = ['sum(net_pay) as sum'],
|
||||
filters = {'employee_name' : self.employee_name,
|
||||
'start_date' : ['>=', period_start_date],
|
||||
'end_date' : ['<', period_end_date]})
|
||||
|
||||
'end_date' : ['<', period_end_date],
|
||||
'name': ['!=', self.name],
|
||||
'docstatus': 1
|
||||
})
|
||||
|
||||
year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
|
||||
|
||||
@ -1160,7 +1170,9 @@ class SalarySlip(TransactionBase):
|
||||
fields = ['sum(net_pay) as sum'],
|
||||
filters = {'employee_name' : self.employee_name,
|
||||
'start_date' : ['>=', first_day_of_the_month],
|
||||
'end_date' : ['<', self.start_date]
|
||||
'end_date' : ['<', self.start_date],
|
||||
'name': ['!=', self.name],
|
||||
'docstatus': 1
|
||||
})
|
||||
|
||||
month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
|
||||
|
@ -318,7 +318,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
year_to_date = 0
|
||||
for slip in salary_slips:
|
||||
year_to_date += slip.net_pay
|
||||
year_to_date += flt(slip.net_pay)
|
||||
self.assertEqual(slip.year_to_date, year_to_date)
|
||||
|
||||
def test_tax_for_payroll_period(self):
|
||||
|
@ -543,6 +543,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
company: me.frm.doc.company,
|
||||
order_type: me.frm.doc.order_type,
|
||||
is_pos: cint(me.frm.doc.is_pos),
|
||||
is_return: cint(me.frm.doc.is_return),
|
||||
is_subcontracted: me.frm.doc.is_subcontracted,
|
||||
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
|
||||
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,
|
||||
|
@ -48,9 +48,6 @@ def validate_regional(doc):
|
||||
|
||||
def missing(field_label, regulation):
|
||||
"""Notify the user that a required field is missing."""
|
||||
context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.'
|
||||
msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format(
|
||||
field_label=frappe.bold(_(field_label)),
|
||||
regulation=regulation
|
||||
)
|
||||
)
|
||||
translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501
|
||||
formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
|
||||
msgprint(formatted_msg)
|
||||
|
12
erpnext/regional/germany/test_accounts_controller.py
Normal file
12
erpnext/regional/germany/test_accounts_controller.py
Normal file
@ -0,0 +1,12 @@
|
||||
import frappe
|
||||
import unittest
|
||||
from erpnext.regional.germany.accounts_controller import validate_regional
|
||||
|
||||
|
||||
class TestAccountsController(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.sales_invoice = frappe.get_last_doc('Sales Invoice')
|
||||
|
||||
def test_validate_regional(self):
|
||||
validate_regional(self.sales_invoice)
|
@ -15,7 +15,7 @@ from frappe import _, bold
|
||||
from pyqrcode import create as qrcreate
|
||||
from frappe.integrations.utils import make_post_request, make_get_request
|
||||
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
|
||||
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date
|
||||
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
|
||||
|
||||
def validate_einvoice_fields(doc):
|
||||
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
|
||||
@ -84,29 +84,32 @@ def get_doc_details(invoice):
|
||||
))
|
||||
|
||||
def get_party_details(address_name):
|
||||
address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
|
||||
gstin = address.get('gstin')
|
||||
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
|
||||
|
||||
gstin_details = get_gstin_details(gstin)
|
||||
legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName')
|
||||
location = gstin_details.get('AddrLoc') or address.get('city')
|
||||
state_code = gstin_details.get('StateCode')
|
||||
pincode = gstin_details.get('AddrPncd')
|
||||
address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno'))
|
||||
address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt'))
|
||||
email_id = address.get('email_id')
|
||||
phone = address.get('phone')
|
||||
# get last 10 digit
|
||||
phone = phone.replace(" ", "")[-10:] if phone else ''
|
||||
if (not d.gstin
|
||||
or not d.city
|
||||
or not d.pincode
|
||||
or not d.address_title
|
||||
or not d.address_line1
|
||||
or not d.gst_state_number):
|
||||
|
||||
if state_code == 97:
|
||||
frappe.throw(
|
||||
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
|
||||
get_link_to_form('Address', address_name)
|
||||
),
|
||||
title=_('Missing Address Fields')
|
||||
)
|
||||
|
||||
if d.gst_state_number == 97:
|
||||
# according to einvoice standard
|
||||
pincode = 999999
|
||||
|
||||
return frappe._dict(dict(
|
||||
gstin=gstin, legal_name=legal_name, location=location,
|
||||
pincode=pincode, state_code=state_code, address_line1=address_line1,
|
||||
address_line2=address_line2, email=email_id, phone=phone
|
||||
gstin=d.gstin, legal_name=d.address_title,
|
||||
location=d.city, pincode=d.pincode,
|
||||
state_code=d.gst_state_number,
|
||||
address_line1=d.address_line1,
|
||||
address_line2=d.address_line2
|
||||
))
|
||||
|
||||
def get_gstin_details(gstin):
|
||||
@ -127,14 +130,22 @@ def get_gstin_details(gstin):
|
||||
return GSPConnector.get_gstin_details(gstin)
|
||||
|
||||
def get_overseas_address_details(address_name):
|
||||
address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value(
|
||||
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id']
|
||||
address_title, address_line1, address_line2, city = frappe.db.get_value(
|
||||
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
|
||||
)
|
||||
|
||||
if not address_title or not address_line1 or not city:
|
||||
frappe.throw(
|
||||
msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
|
||||
get_link_to_form('Address', address_name)
|
||||
),
|
||||
title=_('Missing Address Fields')
|
||||
)
|
||||
|
||||
return frappe._dict(dict(
|
||||
gstin='URP', legal_name=address_title, address_line1=address_line1,
|
||||
address_line2=address_line2, email=email_id, phone=phone,
|
||||
pincode=999999, state_code=96, place_of_supply=96, location=city
|
||||
gstin='URP', legal_name=address_title, location=city,
|
||||
address_line1=address_line1, address_line2=address_line2,
|
||||
pincode=999999, state_code=96, place_of_supply=96
|
||||
))
|
||||
|
||||
def get_item_list(invoice):
|
||||
@ -146,9 +157,10 @@ def get_item_list(invoice):
|
||||
item.update(d.as_dict())
|
||||
|
||||
item.sr_no = d.idx
|
||||
item.discount_amount = abs(item.discount_amount * item.qty)
|
||||
item.description = d.item_name
|
||||
item.description = d.item_name.replace('"', '\\"')
|
||||
|
||||
item.qty = abs(item.qty)
|
||||
item.discount_amount = abs(item.discount_amount * item.qty)
|
||||
item.unit_rate = abs(item.base_amount / item.qty)
|
||||
item.gross_amount = abs(item.base_amount)
|
||||
item.taxable_value = abs(item.base_amount)
|
||||
@ -156,6 +168,7 @@ def get_item_list(invoice):
|
||||
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
||||
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
||||
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
|
||||
item.serial_no = ""
|
||||
|
||||
item = update_item_taxes(invoice, item)
|
||||
|
||||
@ -272,7 +285,25 @@ def get_eway_bill_details(invoice):
|
||||
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
|
||||
))
|
||||
|
||||
def validate_mandatory_fields(invoice):
|
||||
if not invoice.company_address:
|
||||
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
|
||||
if not invoice.customer_address:
|
||||
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
|
||||
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
|
||||
frappe.throw(
|
||||
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
||||
title=_('Missing Fields')
|
||||
)
|
||||
if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
|
||||
frappe.throw(
|
||||
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
|
||||
title=_('Missing Fields')
|
||||
)
|
||||
|
||||
def make_einvoice(invoice):
|
||||
validate_mandatory_fields(invoice)
|
||||
|
||||
schema = read_json('einv_template')
|
||||
|
||||
transaction_details = get_transaction_details(invoice)
|
||||
|
@ -74,67 +74,71 @@ frappe.query_reports["Sales Analytics"] = {
|
||||
return Object.assign(options, {
|
||||
checkboxColumn: true,
|
||||
events: {
|
||||
onCheckRow: function(data) {
|
||||
onCheckRow: function (data) {
|
||||
if (!data) return;
|
||||
const data_doctype = $(
|
||||
data[2].html
|
||||
)[0].attributes.getNamedItem("data-doctype").value;
|
||||
const tree_type = frappe.query_report.filters[0].value;
|
||||
if (data_doctype != tree_type) return;
|
||||
|
||||
row_name = data[2].content;
|
||||
length = data.length;
|
||||
|
||||
var tree_type = frappe.query_report.filters[0].value;
|
||||
|
||||
if(tree_type == "Customer") {
|
||||
row_values = data.slice(4,length-1).map(function (column) {
|
||||
return column.content;
|
||||
})
|
||||
if (tree_type == "Customer") {
|
||||
row_values = data
|
||||
.slice(4, length - 1)
|
||||
.map(function (column) {
|
||||
return column.content;
|
||||
});
|
||||
} else if (tree_type == "Item") {
|
||||
row_values = data.slice(5,length-1).map(function (column) {
|
||||
return column.content;
|
||||
})
|
||||
}
|
||||
else {
|
||||
row_values = data.slice(3,length-1).map(function (column) {
|
||||
return column.content;
|
||||
})
|
||||
row_values = data
|
||||
.slice(5, length - 1)
|
||||
.map(function (column) {
|
||||
return column.content;
|
||||
});
|
||||
} else {
|
||||
row_values = data
|
||||
.slice(3, length - 1)
|
||||
.map(function (column) {
|
||||
return column.content;
|
||||
});
|
||||
}
|
||||
|
||||
entry = {
|
||||
'name':row_name,
|
||||
'values':row_values
|
||||
}
|
||||
name: row_name,
|
||||
values: row_values,
|
||||
};
|
||||
|
||||
let raw_data = frappe.query_report.chart.data;
|
||||
let new_datasets = raw_data.datasets;
|
||||
|
||||
var found = false;
|
||||
|
||||
for(var i=0; i < new_datasets.length;i++){
|
||||
if(new_datasets[i].name == row_name){
|
||||
found = true;
|
||||
new_datasets.splice(i,1);
|
||||
break;
|
||||
let element_found = new_datasets.some((element, index, array)=>{
|
||||
if(element.name == row_name){
|
||||
array.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if(!found){
|
||||
if (!element_found) {
|
||||
new_datasets.push(entry);
|
||||
}
|
||||
|
||||
let new_data = {
|
||||
labels: raw_data.labels,
|
||||
datasets: new_datasets
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
frappe.query_report.chart.update(new_data)
|
||||
}, 500)
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
frappe.query_report.chart.draw(true);
|
||||
}, 1000)
|
||||
datasets: new_datasets,
|
||||
};
|
||||
chart_options = {
|
||||
data: new_data,
|
||||
type: "line",
|
||||
};
|
||||
frappe.query_report.render_chart(chart_options);
|
||||
|
||||
frappe.query_report.raw_chart_data = new_data;
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -8,26 +8,32 @@
|
||||
"field_order": [
|
||||
"specification",
|
||||
"value",
|
||||
"non_numeric",
|
||||
"column_break_3",
|
||||
"min_value",
|
||||
"max_value",
|
||||
"formula_based_criteria",
|
||||
"acceptance_formula"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "specification",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Parameter",
|
||||
"oldfieldname": "specification",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Quality Inspection Parameter",
|
||||
"print_width": "200px",
|
||||
"reqd": 1,
|
||||
"width": "200px"
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
|
||||
"fieldname": "value",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Acceptance Criteria",
|
||||
"label": "Acceptance Criteria Value",
|
||||
"oldfieldname": "value",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
@ -36,17 +42,45 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
|
||||
"depends_on": "formula_based_criteria",
|
||||
"description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
|
||||
"fieldname": "acceptance_formula",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Acceptance Criteria Formula"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "formula_based_criteria",
|
||||
"fieldtype": "Check",
|
||||
"label": "Formula Based Criteria"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
|
||||
"fieldname": "min_value",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Minimum Value"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
|
||||
"fieldname": "max_value",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Maximum Value"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "non_numeric",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Non-Numeric",
|
||||
"width": "80px"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-16 16:33:42.421842",
|
||||
"modified": "2021-01-07 21:32:49.866439",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Quality Inspection Parameter",
|
||||
|
@ -4,6 +4,55 @@
|
||||
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
|
||||
|
||||
frappe.ui.form.on("Quality Inspection", {
|
||||
|
||||
setup: function(frm) {
|
||||
frm.set_query("batch_no", function() {
|
||||
return {
|
||||
filters: {
|
||||
"item": frm.doc.item_code
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Serial No based on item_code
|
||||
frm.set_query("item_serial_no", function() {
|
||||
let filters = {};
|
||||
if (frm.doc.item_code) {
|
||||
filters = {
|
||||
'item_code': frm.doc.item_code
|
||||
};
|
||||
}
|
||||
return { filters: filters };
|
||||
});
|
||||
|
||||
// item code based on GRN/DN
|
||||
frm.set_query("item_code", function(doc) {
|
||||
let doctype = doc.reference_type;
|
||||
|
||||
if (doc.reference_type !== "Job Card") {
|
||||
doctype = (doc.reference_type == "Stock Entry") ?
|
||||
"Stock Entry Detail" : doc.reference_type + " Item";
|
||||
}
|
||||
|
||||
if (doc.reference_type && doc.reference_name) {
|
||||
let filters = {
|
||||
"from": doctype,
|
||||
"inspection_type": doc.inspection_type
|
||||
};
|
||||
|
||||
if (doc.reference_type == doctype)
|
||||
filters["reference_name"] = doc.reference_name;
|
||||
else
|
||||
filters["parent"] = doc.reference_name;
|
||||
|
||||
return {
|
||||
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
|
||||
filters: filters
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
// Ignore cancellation of reference doctype on cancel all.
|
||||
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
|
||||
@ -31,55 +80,5 @@ frappe.ui.form.on("Quality Inspection", {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// item code based on GRN/DN
|
||||
cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
|
||||
let doctype = doc.reference_type;
|
||||
|
||||
if (doc.reference_type !== "Job Card") {
|
||||
doctype = (doc.reference_type == "Stock Entry") ?
|
||||
"Stock Entry Detail" : doc.reference_type + " Item";
|
||||
}
|
||||
|
||||
if (doc.reference_type && doc.reference_name) {
|
||||
let filters = {
|
||||
"from": doctype,
|
||||
"inspection_type": doc.inspection_type
|
||||
};
|
||||
|
||||
if (doc.reference_type == doctype)
|
||||
filters["reference_name"] = doc.reference_name;
|
||||
else
|
||||
filters["parent"] = doc.reference_name;
|
||||
|
||||
return {
|
||||
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
|
||||
filters: filters
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Serial No based on item_code
|
||||
cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {
|
||||
var filters = {};
|
||||
if (doc.item_code) {
|
||||
filters = {
|
||||
'item_code': doc.item_code
|
||||
}
|
||||
}
|
||||
return { filters: filters }
|
||||
}
|
||||
|
||||
cur_frm.set_query("batch_no", function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"item": doc.item_code
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cur_frm.add_fetch('item_code', 'item_name', 'item_name');
|
||||
cur_frm.add_fetch('item_code', 'description', 'description');
|
||||
|
||||
},
|
||||
});
|
@ -136,6 +136,7 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -143,6 +144,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.description",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
@ -236,7 +238,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-19 17:06:05.409963",
|
||||
"modified": "2020-12-18 19:59:55.710300",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection",
|
||||
|
@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import flt, cint
|
||||
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
|
||||
import get_template_details
|
||||
|
||||
@ -16,7 +16,7 @@ class QualityInspection(Document):
|
||||
self.get_item_specification_details()
|
||||
|
||||
if self.readings:
|
||||
self.set_status_based_on_acceptance_formula()
|
||||
self.inspect_and_set_status()
|
||||
|
||||
def get_item_specification_details(self):
|
||||
if not self.quality_inspection_template:
|
||||
@ -29,9 +29,7 @@ class QualityInspection(Document):
|
||||
parameters = get_template_details(self.quality_inspection_template)
|
||||
for d in parameters:
|
||||
child = self.append('readings', {})
|
||||
child.specification = d.specification
|
||||
child.value = d.value
|
||||
child.acceptance_formula = d.acceptance_formula
|
||||
child.update(d)
|
||||
child.status = "Accepted"
|
||||
|
||||
def get_quality_inspection_template(self):
|
||||
@ -69,35 +67,98 @@ class QualityInspection(Document):
|
||||
doctype = 'Stock Entry Detail'
|
||||
|
||||
if self.reference_type and self.reference_name:
|
||||
conditions = ""
|
||||
if self.batch_no and self.docstatus == 1:
|
||||
conditions += " and t1.batch_no = '%s'"%(self.batch_no)
|
||||
|
||||
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
|
||||
conditions += " and t1.quality_inspection = '%s'"%(self.name)
|
||||
|
||||
frappe.db.sql("""
|
||||
UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2
|
||||
SET t1.quality_inspection = %s, t2.modified = %s
|
||||
WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name
|
||||
""".format(parent_doc=self.reference_type, child_doc=doctype),
|
||||
UPDATE
|
||||
`tab{child_doc}` t1, `tab{parent_doc}` t2
|
||||
SET
|
||||
t1.quality_inspection = %s, t2.modified = %s
|
||||
WHERE
|
||||
t1.parent = %s
|
||||
and t1.item_code = %s
|
||||
and t1.parent = t2.name
|
||||
{conditions}
|
||||
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
|
||||
(quality_inspection, self.modified, self.reference_name, self.item_code))
|
||||
|
||||
def set_status_based_on_acceptance_formula(self):
|
||||
def inspect_and_set_status(self):
|
||||
for reading in self.readings:
|
||||
if not reading.acceptance_formula: continue
|
||||
if not reading.manual_inspection: # dont auto set status if manual
|
||||
if reading.formula_based_criteria:
|
||||
self.set_status_based_on_acceptance_formula(reading)
|
||||
else:
|
||||
# if not formula based check acceptance values set
|
||||
self.set_status_based_on_acceptance_values(reading)
|
||||
|
||||
condition = reading.acceptance_formula
|
||||
data = {}
|
||||
def set_status_based_on_acceptance_values(self, reading):
|
||||
if cint(reading.non_numeric):
|
||||
result = reading.get("reading_value") == reading.get("value")
|
||||
else:
|
||||
# numeric readings
|
||||
result = self.min_max_criteria_passed(reading)
|
||||
|
||||
reading.status = "Accepted" if result else "Rejected"
|
||||
|
||||
def min_max_criteria_passed(self, reading):
|
||||
"""Determine whether all readings fall in the acceptable range."""
|
||||
for i in range(1, 11):
|
||||
reading_value = reading.get("reading_" + str(i))
|
||||
if reading_value is not None and reading_value.strip():
|
||||
result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
|
||||
if not result: return False
|
||||
return True
|
||||
|
||||
def set_status_based_on_acceptance_formula(self, reading):
|
||||
if not reading.acceptance_formula:
|
||||
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
|
||||
title=_("Missing Formula"))
|
||||
|
||||
condition = reading.acceptance_formula
|
||||
data = self.get_formula_evaluation_data(reading)
|
||||
|
||||
try:
|
||||
result = frappe.safe_eval(condition, None, data)
|
||||
reading.status = "Accepted" if result else "Rejected"
|
||||
except NameError as e:
|
||||
field = frappe.bold(e.args[0].split()[1])
|
||||
frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
|
||||
.format(reading.idx, field),
|
||||
title=_("Invalid Formula"))
|
||||
except Exception:
|
||||
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
|
||||
title=_("Invalid Formula"))
|
||||
|
||||
def get_formula_evaluation_data(self, reading):
|
||||
data = {}
|
||||
if cint(reading.non_numeric):
|
||||
data = {"reading_value": reading.get("reading_value")}
|
||||
else:
|
||||
# numeric readings
|
||||
for i in range(1, 11):
|
||||
field = "reading_" + str(i)
|
||||
data[field] = flt(reading.get(field)) or 0
|
||||
data[field] = flt(reading.get(field))
|
||||
data["mean"] = self.calculate_mean(reading)
|
||||
|
||||
try:
|
||||
result = frappe.safe_eval(condition, None, data)
|
||||
reading.status = "Accepted" if result else "Rejected"
|
||||
except SyntaxError:
|
||||
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
|
||||
title=_("Invalid Formula"))
|
||||
except NameError as e:
|
||||
field = frappe.bold(e.args[0].split()[1])
|
||||
frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
|
||||
.format(reading.idx, field),
|
||||
title=_("Invalid Formula"))
|
||||
return data
|
||||
|
||||
def calculate_mean(self, reading):
|
||||
"""Calculate mean of all non-empty readings."""
|
||||
from statistics import mean
|
||||
readings_list = []
|
||||
|
||||
for i in range(1, 11):
|
||||
reading_value = reading.get("reading_" + str(i))
|
||||
if reading_value is not None and reading_value.strip():
|
||||
readings_list.append(flt(reading_value))
|
||||
|
||||
actual_mean = mean(readings_list) if readings_list else 0
|
||||
return actual_mean
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
|
@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase):
|
||||
qa.delete()
|
||||
dn.delete()
|
||||
|
||||
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
|
||||
"non_numeric": 1,
|
||||
"value": "Yes",
|
||||
"reading_value": "Yes"
|
||||
}]
|
||||
|
||||
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
|
||||
self.assertEqual(qa.readings[0].status, "Accepted")
|
||||
self.assertEqual(qa.readings[1].status, "Accepted")
|
||||
|
||||
qa.delete()
|
||||
dn.delete()
|
||||
|
||||
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",
|
||||
"specification": "Iron Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
|
||||
"reading_1": 0.4
|
||||
"reading_1": "0.4"
|
||||
},
|
||||
{
|
||||
"specification": "Calcium Content",
|
||||
"specification": "Calcium Content", # numeric reading
|
||||
"formula_based_criteria": 1,
|
||||
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
|
||||
"reading_1": 0.7
|
||||
"reading_1": "0.7"
|
||||
},
|
||||
{
|
||||
"specification": "Mg Content",
|
||||
"acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9",
|
||||
"reading_1": 0.5,
|
||||
"reading_2": 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,
|
||||
"non_numeric": 1,
|
||||
"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,
|
||||
@ -72,6 +109,7 @@ class TestQualityInspection(unittest.TestCase):
|
||||
self.assertEqual(qa.readings[0].status, "Accepted")
|
||||
self.assertEqual(qa.readings[1].status, "Rejected")
|
||||
self.assertEqual(qa.readings[2].status, "Accepted")
|
||||
self.assertEqual(qa.readings[3].status, "Accepted")
|
||||
|
||||
qa.delete()
|
||||
dn.delete()
|
||||
@ -86,11 +124,20 @@ def create_quality_inspection(**args):
|
||||
qa.item_code = args.item_code or "_Test Item with QA"
|
||||
qa.sample_size = 1
|
||||
qa.inspected_by = frappe.session.user
|
||||
qa.status = args.status or "Accepted"
|
||||
|
||||
readings = args.readings or {"specification": "Size", "status": args.status}
|
||||
if not args.readings:
|
||||
create_quality_inspection_parameter("Size")
|
||||
readings = {"specification": "Size", "min_value": 0, "max_value": 10}
|
||||
else:
|
||||
readings = args.readings
|
||||
|
||||
if args.status == "Rejected":
|
||||
readings["reading_1"] = "12" # status is auto set in child on save
|
||||
|
||||
if isinstance(readings, list):
|
||||
for entry in readings:
|
||||
create_quality_inspection_parameter(entry["specification"])
|
||||
qa.append("readings", entry)
|
||||
else:
|
||||
qa.append("readings", readings)
|
||||
@ -101,3 +148,11 @@ def create_quality_inspection(**args):
|
||||
qa.submit()
|
||||
|
||||
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()
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Quality Inspection Parameter', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
@ -0,0 +1,86 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "field:parameter",
|
||||
"creation": "2020-12-28 17:06:00.254129",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"parameter",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "parameter",
|
||||
"fieldtype": "Data",
|
||||
"label": "Parameter",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-28 18:06:54.897317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection Parameter",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Quality Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class QualityInspectionParameter(Document):
|
||||
pass
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestQualityInspectionParameter(unittest.TestCase):
|
||||
pass
|
@ -7,21 +7,28 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"specification",
|
||||
"value",
|
||||
"status",
|
||||
"value",
|
||||
"non_numeric",
|
||||
"manual_inspection",
|
||||
"column_break_4",
|
||||
"min_value",
|
||||
"max_value",
|
||||
"formula_based_criteria",
|
||||
"acceptance_formula",
|
||||
"section_break_3",
|
||||
"reading_value",
|
||||
"section_break_14",
|
||||
"reading_1",
|
||||
"reading_2",
|
||||
"reading_3",
|
||||
"column_break_10",
|
||||
"reading_4",
|
||||
"column_break_10",
|
||||
"reading_5",
|
||||
"reading_6",
|
||||
"column_break_14",
|
||||
"reading_7",
|
||||
"reading_8",
|
||||
"column_break_14",
|
||||
"reading_9",
|
||||
"reading_10"
|
||||
],
|
||||
@ -29,19 +36,20 @@
|
||||
{
|
||||
"columns": 3,
|
||||
"fieldname": "specification",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Parameter",
|
||||
"oldfieldname": "specification",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Quality Inspection Parameter",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
|
||||
"fieldname": "value",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Acceptance Criteria",
|
||||
"label": "Acceptance Criteria Value",
|
||||
"oldfieldname": "value",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
@ -67,7 +75,6 @@
|
||||
"columns": 1,
|
||||
"fieldname": "reading_3",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Reading 3",
|
||||
"oldfieldname": "reading_3",
|
||||
"oldfieldtype": "Data"
|
||||
@ -133,15 +140,18 @@
|
||||
"options": "Accepted\nRejected"
|
||||
},
|
||||
{
|
||||
"depends_on": "non_numeric",
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Value Based Inspection"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
|
||||
"depends_on": "formula_based_criteria",
|
||||
"description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
|
||||
"fieldname": "acceptance_formula",
|
||||
"fieldtype": "Code",
|
||||
"label": "Acceptance Criteria Formula"
|
||||
@ -153,12 +163,59 @@
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "formula_based_criteria",
|
||||
"fieldtype": "Check",
|
||||
"label": "Formula Based Criteria"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
|
||||
"description": "Applied on each reading.",
|
||||
"fieldname": "min_value",
|
||||
"fieldtype": "Float",
|
||||
"label": "Minimum Value"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
|
||||
"description": "Applied on each reading.",
|
||||
"fieldname": "max_value",
|
||||
"fieldtype": "Float",
|
||||
"label": "Maximum Value"
|
||||
},
|
||||
{
|
||||
"depends_on": "non_numeric",
|
||||
"fieldname": "reading_value",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Reading Value"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.non_numeric",
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Numeric Inspection"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "non_numeric",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Non-Numeric"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Set the status manually.",
|
||||
"fieldname": "manual_inspection",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manual Inspection"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-16 16:34:29.947856",
|
||||
"modified": "2021-01-07 21:56:40.235579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection Reading",
|
||||
|
@ -13,6 +13,7 @@ def get_template_details(template):
|
||||
if not template: return []
|
||||
|
||||
return frappe.get_all('Item Quality Inspection Parameter',
|
||||
fields=["specification", "value", "acceptance_formula"],
|
||||
fields=["specification", "value", "acceptance_formula",
|
||||
"non_numeric", "formula_based_criteria", "min_value", "max_value"],
|
||||
filters={'parenttype': 'Quality Inspection Template', 'parent': template},
|
||||
order_by="idx")
|
@ -74,7 +74,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
|
||||
update_party_blanket_order(args, out)
|
||||
|
||||
get_price_list_rate(args, item, out)
|
||||
if not doc or cint(doc.get('is_return')) == 0:
|
||||
# get price list rate only if the invoice is not a credit or debit note
|
||||
get_price_list_rate(args, item, out)
|
||||
|
||||
if args.customer and cint(args.is_pos):
|
||||
out.update(get_pos_profile_item_details(args.company, args))
|
||||
|
Loading…
Reference in New Issue
Block a user