fix: resolved commit

This commit is contained in:
Anupam
2021-01-11 17:55:29 +05:30
55 changed files with 1024 additions and 374 deletions

View File

@@ -159,10 +159,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@@ -181,10 +181,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")

View File

@@ -267,6 +267,8 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile: if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {} pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name') self.pos_profile = pos_profile.get('name')
profile = {} profile = {}

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@@ -549,7 +549,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc) self.against_income_account = ','.join(against_acc)
def add_remarks(self): def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks' if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended

View File

@@ -75,62 +75,70 @@ frappe.query_reports["Purchase Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { 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; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Supplier") {
row_values = data
if(tree_type == "Supplier" || tree_type == "Item") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} } else if (tree_type == "Item") {
else { row_values = data
row_values = data.slice(3,length-1).map(function (column) { .slice(5, length - 1)
return column.content; .map(function (column) {
}) return column.content;
});
} else {
row_values = data
.slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
},500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}); });
} }
} }

View File

@@ -328,6 +328,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) 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.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name target_doc.sales_invoice_item = source_doc.name
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@@ -233,7 +233,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate 'incoming_rate': p.get("incoming_rate")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@@ -252,7 +252,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate 'incoming_rate': d.get("incoming_rate")
})) }))
return il return il

View File

@@ -6,6 +6,7 @@ import unittest
from erpnext.stock.doctype.item.test_item import set_item_variant_settings 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.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 from six import string_types
@@ -56,6 +57,8 @@ def make_quality_inspection_template():
qc = frappe.new_doc("Quality Inspection Template") qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template qc.quality_inspection_template_name = qc_template
create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', { qc.append('item_quality_inspection_parameter', {
"specification": "Moisture", "specification": "Moisture",
"value": "&lt; 5%", "value": "&lt; 5%",

View File

@@ -17,6 +17,8 @@
"enable_free_follow_ups", "enable_free_follow_ups",
"max_visits", "max_visits",
"valid_days", "valid_days",
"inpatient_settings_section",
"allow_discharge_despite_unbilled_services",
"healthcare_service_items", "healthcare_service_items",
"inpatient_visit_charge_item", "inpatient_visit_charge_item",
"op_consulting_charge_item", "op_consulting_charge_item",
@@ -302,11 +304,22 @@
"fieldname": "enable_free_follow_ups", "fieldname": "enable_free_follow_ups",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Free Follow-ups" "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, "issingle": 1,
"links": [], "links": [],
"modified": "2020-07-08 15:17:21.543218", "modified": "2021-01-04 10:19:22.329272",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Settings", "name": "Healthcare Settings",

View File

@@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _ 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.model.document import Document
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
@@ -113,6 +113,7 @@ def schedule_inpatient(args):
inpatient_record.status = 'Admission Scheduled' inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
def schedule_discharge(args): def schedule_discharge(args):
discharge_order = json.loads(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', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, '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): def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order: for key in ip_order:
inpatient_record.set(key, ip_order[key]) inpatient_record.set(key, ip_order[key])
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child: for item in encounter_child:
table = inpatient_record.append(inpatient_record_child) table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'): for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname)) table.set(df.fieldname, item.get(df.fieldname))
def check_out_inpatient(inpatient_record): def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in 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() inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
def discharge_patient(inpatient_record): def discharge_patient(inpatient_record):
validate_invoiced_inpatient(inpatient_record) validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today() inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged" inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True) 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: if inpatient_record.inpatient_occupancies:
service_unit_names = False service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
if inpatient_occupancy.invoiced != 1: if not inpatient_occupancy.invoiced:
if service_unit_names: if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit service_unit_names += ", " + inpatient_occupancy.service_unit
else: else:
service_unit_names = inpatient_occupancy.service_unit service_unit_names = inpatient_occupancy.service_unit
if service_unit_names: 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"] docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs: 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: if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices) pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
if pending_invoices: return pending_invoices
frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
.join(pending_invoices)), title=_('Unbilled Invoices'))
def get_pending_doc(doc, doc_name_list, pending_invoices): def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list: if doc_name_list:
doc_ids = False doc_ids = False
for doc_name in doc_name_list: for doc_name in doc_name_list:
doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids: if doc_ids:
doc_ids += ", "+doc_name.name doc_ids += ", " + doc_link
else: else:
doc_ids = doc_name.name doc_ids = doc_link
if doc_ids: if doc_ids:
pending_invoices.append(doc + " (" + doc_ids + ")") pending_invoices[doc] = doc_ids
return pending_invoices 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, return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted' 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_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
def transfer_patient(inpatient_record, service_unit, check_in): def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {}) item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit 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") frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
def patient_leave_service_unit(inpatient_record, check_out, leave_from): def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in 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") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters): def get_leave_from(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -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_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) 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): def test_validate_overlap_admission(self):
frappe.db.sql("""delete from `tabInpatient Record`""") frappe.db.sql("""delete from `tabInpatient Record`""")
patient = create_patient() patient = create_patient()
@@ -63,6 +88,13 @@ def mark_invoiced_inpatient_occupancy(ip_record):
inpatient_occupancy.invoiced = 1 inpatient_occupancy.invoiced = 1
ip_record.save(ignore_permissions = True) 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): def create_inpatient(patient):
patient_obj = frappe.get_doc('Patient', patient) patient_obj = frappe.get_doc('Patient', patient)
inpatient_record = frappe.new_doc('Inpatient Record') inpatient_record = frappe.new_doc('Inpatient Record')
@@ -78,6 +110,7 @@ def create_inpatient(patient):
inpatient_record.scheduled_date = today() inpatient_record.scheduled_date = today()
return inpatient_record return inpatient_record
def get_healthcare_service_unit(): def get_healthcare_service_unit():
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
if not service_unit: if not service_unit:
@@ -105,6 +138,7 @@ def get_healthcare_service_unit():
return service_unit.name return service_unit.name
return service_unit return service_unit
def get_service_unit_type(): def get_service_unit_type():
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1}) 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.name
return service_unit_type return service_unit_type
def create_patient(): def create_patient():
patient = frappe.db.exists('Patient', '_Test IPD Patient') patient = frappe.db.exists('Patient', '_Test IPD Patient')
if not patient: if not patient:

View File

@@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEquals(appointment.status, 'Open') self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled') self.assertEquals(appointment.status, 'Scheduled')
create_encounter(appointment) encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') 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): def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()

View File

@@ -5,10 +5,10 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest 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_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.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): class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self): def test_creation_on_encounter_submission(self):
@@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit() frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') 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): def test_therapy_plan_from_template(self):
patient = create_patient() patient = create_patient()
template = create_therapy_plan_template() template = create_therapy_plan_template()

View File

@@ -47,7 +47,7 @@ class TherapyPlan(Document):
@frappe.whitelist() @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_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session') 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.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
if frappe.flags.in_test: if frappe.flags.in_test:
therapy_session.start_date = today() therapy_session.start_date = today()

View File

@@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', {
} }
}; };
}); });
frm.set_query('appointment', function() {
return {
filters: {
'status': ['in', ['Open', 'Scheduled']]
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@@ -43,7 +43,14 @@ class TherapySession(Document):
self.update_sessions_count_in_therapy_plan() self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self) 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): 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) self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False): def update_sessions_count_in_therapy_plan(self, on_cancel=False):

View File

@@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-10-16 15:02:04.283657", "modified": "2021-01-01 16:54:33.477439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",
@@ -855,7 +855,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"search_fields": "employee_name", "search_fields": "employee_name",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@@ -11,6 +11,7 @@
"employee", "employee",
"employee_name", "employee_name",
"department", "department",
"company",
"column_break1", "column_break1",
"leave_type", "leave_type",
"from_date", "from_date",
@@ -219,6 +220,15 @@
"label": "Leave Policy Assignment", "label": "Leave Policy Assignment",
"options": "Leave Policy Assignment", "options": "Leave Policy Assignment",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
} }
], ],
"icon": "fa fa-ok", "icon": "fa fa-ok",
@@ -226,7 +236,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-20 14:25:10.314323", "modified": "2021-01-04 18:46:13.184104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-05-09 15:47:39.760406", "creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@@ -8,6 +9,7 @@
"leave_type", "leave_type",
"transaction_type", "transaction_type",
"transaction_name", "transaction_name",
"company",
"leaves", "leaves",
"column_break_7", "column_break_7",
"from_date", "from_date",
@@ -106,12 +108,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Holiday List", "label": "Holiday List",
"options": "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, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2020-09-04 12:16:36.569066", "links": [],
"modified": "2021-01-04 18:47:45.146652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Ledger Entry", "name": "Leave Ledger Entry",

View File

@@ -111,13 +111,14 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-17 16:27:20.311060", "modified": "2020-12-31 16:43:30.695206",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Policy Assignment", "name": "Leave Policy Assignment",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@@ -131,6 +132,7 @@
"write": 1 "write": 1
}, },
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@@ -144,6 +146,7 @@
"write": 1 "write": 1
}, },
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,

View File

@@ -362,6 +362,27 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db() unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1) 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): def test_disbursal_check_with_shortfall(self):
pledges = [{ pledges = [{
"loan_security": "Test Security 2", "loan_security": "Test Security 2",

View File

@@ -44,10 +44,16 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime()) "valid_upto": (">=", get_datetime())
}, as_list=1)) }, as_list=1))
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable', 'written_off_amount']) '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 security_value = 0
unpledge_qty_map = {} unpledge_qty_map = {}
ltv_ratio = 0 ltv_ratio = 0

View File

@@ -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.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn 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

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

View File

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

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import nowdate from frappe.utils import nowdate, flt
from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
@@ -113,15 +113,15 @@ def execute():
interest_paid = 0 interest_paid = 0
principal_paid = 0 principal_paid = 0
if total_interest > entry.interest_amount: if flt(total_interest) > flt(entry.interest_amount):
interest_paid = entry.interest_amount interest_paid = flt(entry.interest_amount)
else: else:
interest_paid = total_interest interest_paid = flt(total_interest)
if total_principal > entry.payable_principal_amount: if flt(total_principal) > flt(entry.payable_principal_amount):
principal_paid = entry.payable_principal_amount principal_paid = flt(entry.payable_principal_amount)
else: else:
principal_paid = total_principal principal_paid = flt(total_principal)
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` + %s, SET paid_principal_amount = `paid_principal_amount` + %s,
@@ -129,8 +129,8 @@ def execute():
WHERE name = %s""", WHERE name = %s""",
(principal_paid, interest_paid, entry.name)) (principal_paid, interest_paid, entry.name))
total_principal -= principal_paid total_principal = flt(total_principal) - principal_paid
total_interest -= interest_paid total_interest = flt(total_interest) - interest_paid
def create_loan_type(loan, loan_type_name, penalty_account): def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc = frappe.new_doc('Loan Type') loan_type_doc = frappe.new_doc('Loan Type')

View File

@@ -5,11 +5,11 @@ from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
# udpate sales cycle # update sales cycle
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']: for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d) 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']: 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) frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)

View File

@@ -46,7 +46,7 @@ frappe.ui.form.on('Payroll Entry', {
} }
).toggleClass('btn-primary', !(frm.doc.employees || []).length); ).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.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.page.set_primary_action(__('Create Salary Slips'), () => {
frm.save('Submit').then(() => { frm.save('Submit').then(() => {

View File

@@ -21,6 +21,9 @@ class PayrollEntry(Document):
if cint(entries) == len(self.employees): if cint(entries) == len(self.employees):
self.set_onload("submitted_ss", True) self.set_onload("submitted_ss", True)
def validate(self):
self.number_of_employees = len(self.employees)
def on_submit(self): def on_submit(self):
self.create_salary_slips() self.create_salary_slips()
@@ -113,7 +116,7 @@ class PayrollEntry(Document):
for d in employees: for d in employees:
self.append('employees', d) self.append('employees', d)
self.number_of_employees = len(employees) self.number_of_employees = len(self.employees)
if self.validate_attendance: if self.validate_attendance:
return self.validate_employee_attendance() return self.validate_employee_attendance()
@@ -145,8 +148,8 @@ class PayrollEntry(Document):
""" """
self.check_permission('write') self.check_permission('write')
self.created = 1 self.created = 1
emp_list = [d.employee for d in self.get_emp_list()] employees = [emp.employee for emp in self.employees]
if emp_list: if employees:
args = frappe._dict({ args = frappe._dict({
"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
"payroll_frequency": self.payroll_frequency, "payroll_frequency": self.payroll_frequency,
@@ -160,10 +163,10 @@ class PayrollEntry(Document):
"exchange_rate": self.exchange_rate, "exchange_rate": self.exchange_rate,
"currency": self.currency "currency": self.currency
}) })
if len(emp_list) > 30: if len(employees) > 30:
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
else: 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 # since this method is called via frm.call this doc needs to be updated manually
self.reload() self.reload()

View File

@@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase):
frappe.db.sql("delete from `tab%s`" % dt) frappe.db.sql("delete from `tab%s`" % dt)
make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
@@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase):
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
"_Test Payroll Payable - _TC") "_Test Payroll Payable - _TC")
currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
dates = get_start_end_dates('Monthly', nowdate()) dates = get_start_end_dates('Monthly', nowdate())
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):

View File

@@ -151,7 +151,6 @@ frappe.ui.form.on("Salary Slip", {
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; 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['earnings'].grid.set_column_disp(salary_detail_fields, false);
frm.fields_dict['deductions'].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"); frm.trigger("set_dynamic_labels");
}, },
@@ -214,14 +213,16 @@ frappe.ui.form.on('Salary Slip Timesheet', {
}); });
var calculate_totals = function(frm) { var calculate_totals = function(frm) {
if (frm.doc.earnings || frm.doc.deductions) { if (frm.doc.docstatus === 0) {
frappe.call({ if (frm.doc.earnings || frm.doc.deductions) {
method: "set_totals", frappe.call({
doc: frm.doc, method: "set_totals",
callback: function() { doc: frm.doc,
frm.refresh_fields(); callback: function() {
} frm.refresh_fields();
}); }
});
}
} }
}; };

View File

@@ -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.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet() self.set_time_sheet()
self.pull_sal_struct() 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"]) ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
return [payroll_based_on, consider_unmarked_attendance_as] return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
def set_time_sheet(self): def set_time_sheet(self):
if self.salary_slip_based_on_timesheet: if self.salary_slip_based_on_timesheet:
@@ -424,16 +424,19 @@ class SalarySlip(TransactionBase):
def calculate_net_pay(self): def calculate_net_pay(self):
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("earnings") 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')) self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("deductions") 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_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.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay) 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')) 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: else:
self.add_tax_components(payroll_period) self.add_tax_components(payroll_period)
self.set_component_amounts_based_on_payment_days(component_type)
def add_structure_components(self, component_type): def add_structure_components(self, component_type):
data = self.get_data_for_eval() data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type): 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, 'default_amount': amount if not struct_row.get("is_additional_component") else 0,
'depends_on_payment_days' : struct_row.depends_on_payment_days, 'depends_on_payment_days' : struct_row.depends_on_payment_days,
'salary_component' : struct_row.salary_component, '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, 'additional_salary': additional_salary,
'do_not_include_in_total' : struct_row.do_not_include_in_total, 'do_not_include_in_total' : struct_row.do_not_include_in_total,
'is_tax_applicable': struct_row.is_tax_applicable, '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 cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date 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) additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("additional_amount")) / 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 struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row 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 total = 0.0
for d in self.get(component_type): for d in self.get(component_type):
if not d.do_not_include_in_total: if not d.do_not_include_in_total:
d.amount = flt(d.amount, d.precision("amount")) if depends_on_payment_days:
total += d.amount 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 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, joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
@@ -964,8 +971,9 @@ class SalarySlip(TransactionBase):
if not joining_date: if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for d in self.get(component_type): for component_type in ("earnings", "deductions"):
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] 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): def set_loan_repayment(self):
self.total_loan_repayment = 0 self.total_loan_repayment = 0
@@ -1089,17 +1097,17 @@ class SalarySlip(TransactionBase):
self.calculate_net_pay() self.calculate_net_pay()
def set_totals(self): def set_totals(self):
self.gross_pay = 0 self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1: if self.salary_slip_based_on_timesheet == 1:
self.calculate_total_for_salary_slip_based_on_timesheet() self.calculate_total_for_salary_slip_based_on_timesheet()
else: else:
self.total_deduction = 0 self.total_deduction = 0.0
if self.earnings: if self.earnings:
for earning in 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: if self.deductions:
for deduction in 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.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
self.set_base_totals() self.set_base_totals()
@@ -1145,8 +1153,10 @@ class SalarySlip(TransactionBase):
fields = ['sum(net_pay) as sum'], fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name, filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', period_start_date], '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 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'], fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name, filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', first_day_of_the_month], '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 month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0

View File

@@ -318,7 +318,7 @@ class TestSalarySlip(unittest.TestCase):
year_to_date = 0 year_to_date = 0
for slip in salary_slips: 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) self.assertEqual(slip.year_to_date, year_to_date)
def test_tax_for_payroll_period(self): def test_tax_for_payroll_period(self):
@@ -585,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
"amount": 200, "amount": 200,
"exempted_from_income_tax": 1 "exempted_from_income_tax": 1
},
{
"salary_component": 'TDS',
"abbr":'T',
"type": "Deduction",
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
} }
] ]
if not test_tax: if not test_tax:
@@ -603,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
"type": "Deduction", "type": "Deduction",
"round_to_the_nearest_integer": 1 "round_to_the_nearest_integer": 1
}) })
else:
data.append({
"salary_component": 'TDS',
"abbr":'T',
"type": "Deduction",
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
})
if setup or test_tax: if setup or test_tax:
make_salary_component(data, test_tax, company_list) make_salary_component(data, test_tax, company_list)

View File

@@ -43,7 +43,7 @@ class SalaryStructureAssignment(Document):
def set_payroll_payable_account(self): def set_payroll_payable_account(self):
if not self.payroll_payable_account: if not self.payroll_payable_account:
payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account')
if not payroll_payable_account: if not payroll_payable_account:
payroll_payable_account = frappe.db.get_value( payroll_payable_account = frappe.db.get_value(
"Account", { "Account", {

View File

@@ -543,6 +543,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
company: me.frm.doc.company, company: me.frm.doc.company,
order_type: me.frm.doc.order_type, order_type: me.frm.doc.order_type,
is_pos: cint(me.frm.doc.is_pos), is_pos: cint(me.frm.doc.is_pos),
is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted, is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,

View File

@@ -48,9 +48,6 @@ def validate_regional(doc):
def missing(field_label, regulation): def missing(field_label, regulation):
"""Notify the user that a required field is missing.""" """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.' 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
msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format( formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
field_label=frappe.bold(_(field_label)), msgprint(formatted_msg)
regulation=regulation
)
)

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

View File

@@ -15,7 +15,7 @@ from frappe import _, bold
from pyqrcode import create as qrcreate from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request 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 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): def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) 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): def get_party_details(address_name):
address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
gstin = address.get('gstin')
gstin_details = get_gstin_details(gstin) if (not d.gstin
legal_name = gstin_details.get('LegalName') or not d.city
location = gstin_details.get('AddrLoc') or address.get('city') or not d.pincode
state_code = gstin_details.get('StateCode') or not d.address_title
pincode = gstin_details.get('AddrPncd') or not d.address_line1
address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) or not d.gst_state_number):
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 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 # according to einvoice standard
pincode = 999999 pincode = 999999
return frappe._dict(dict( return frappe._dict(dict(
gstin=gstin, legal_name=legal_name, location=location, gstin=d.gstin, legal_name=d.address_title,
pincode=pincode, state_code=state_code, address_line1=address_line1, location=d.city, pincode=d.pincode,
address_line2=address_line2, email=email_id, phone=phone state_code=d.gst_state_number,
address_line1=d.address_line1,
address_line2=d.address_line2
)) ))
def get_gstin_details(gstin): def get_gstin_details(gstin):
@@ -127,14 +130,22 @@ def get_gstin_details(gstin):
return GSPConnector.get_gstin_details(gstin) return GSPConnector.get_gstin_details(gstin)
def get_overseas_address_details(address_name): def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( address_title, address_line1, address_line2, city = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] '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( return frappe._dict(dict(
gstin='URP', legal_name=address_title, address_line1=address_line1, gstin='URP', legal_name=address_title, location=city,
address_line2=address_line2, email=email_id, phone=phone, address_line1=address_line1, address_line2=address_line2,
pincode=999999, state_code=96, place_of_supply=96, location=city pincode=999999, state_code=96, place_of_supply=96
)) ))
def get_item_list(invoice): def get_item_list(invoice):
@@ -146,9 +157,10 @@ def get_item_list(invoice):
item.update(d.as_dict()) item.update(d.as_dict())
item.sr_no = d.idx item.sr_no = d.idx
item.discount_amount = abs(item.discount_amount * item.qty) item.description = d.item_name.replace('"', '\\"')
item.description = d.item_name
item.qty = abs(item.qty) item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_amount / item.qty) item.unit_rate = abs(item.base_amount / item.qty)
item.gross_amount = abs(item.base_amount) item.gross_amount = abs(item.base_amount)
item.taxable_value = 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 = 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.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.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) item = update_item_taxes(invoice, item)
@@ -272,7 +285,25 @@ def get_eway_bill_details(invoice):
vehicle_type=vehicle_type[invoice.gst_vehicle_type] 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): def make_einvoice(invoice):
validate_mandatory_fields(invoice)
schema = read_json('einv_template') schema = read_json('einv_template')
transaction_details = get_transaction_details(invoice) transaction_details = get_transaction_details(invoice)
@@ -351,7 +382,7 @@ def validate_einvoice(validations, einvoice, errors=[]):
# remove empty dicts # remove empty dicts
einvoice.pop(fieldname, None) einvoice.pop(fieldname, None)
continue continue
# convert to int or str # convert to int or str
if value_type == 'string': if value_type == 'string':
einvoice[fieldname] = str(value) einvoice[fieldname] = str(value)

View File

@@ -25,7 +25,6 @@ class Quotation(SellingController):
def validate(self): def validate(self):
super(Quotation, self).validate() super(Quotation, self).validate()
self.set_status() self.set_status()
self.update_opportunity()
self.validate_uom_is_integer("stock_uom", "qty") self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till() self.validate_valid_till()
self.set_customer_name() self.set_customer_name()
@@ -50,21 +49,20 @@ class Quotation(SellingController):
lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"])
self.customer_name = company_name or lead_name self.customer_name = company_name or lead_name
def update_opportunity(self): def update_opportunity(self, status):
for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])): for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])):
if opportunity: if opportunity:
self.update_opportunity_status(opportunity) self.update_opportunity_status(status, opportunity)
if self.opportunity: if self.opportunity:
self.update_opportunity_status() self.update_opportunity_status(status)
def update_opportunity_status(self, opportunity=None): def update_opportunity_status(self, status, opportunity=None):
if not opportunity: if not opportunity:
opportunity = self.opportunity opportunity = self.opportunity
opp = frappe.get_doc("Opportunity", opportunity) opp = frappe.get_doc("Opportunity", opportunity)
opp.status = None opp.set_status(status=status, update=True)
opp.set_status(update=True)
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order(): if not self.has_sales_order():
@@ -82,7 +80,7 @@ class Quotation(SellingController):
else: else:
frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason'))))
self.update_opportunity() self.update_opportunity('Lost')
self.update_lead() self.update_lead()
self.save() self.save()
@@ -95,7 +93,7 @@ class Quotation(SellingController):
self.company, self.base_grand_total, self) self.company, self.base_grand_total, self)
#update enquiry status #update enquiry status
self.update_opportunity() self.update_opportunity('Quotation')
self.update_lead() self.update_lead()
def on_cancel(self): def on_cancel(self):
@@ -105,7 +103,7 @@ class Quotation(SellingController):
#update enquiry status #update enquiry status
self.set_status(update=True) self.set_status(update=True)
self.update_opportunity() self.update_opportunity('Open')
self.update_lead() self.update_lead()
def print_other_charges(self,docname): def print_other_charges(self,docname):

View File

@@ -158,7 +158,6 @@ class SalesOrder(SellingController):
frappe.throw(_("Quotation {0} is cancelled").format(quotation)) frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True) doc.set_status(update=True)
doc.update_opportunity()
def validate_drop_ship(self): def validate_drop_ship(self):
for d in self.get('items'): for d in self.get('items'):
@@ -830,56 +829,49 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers: for supplier in suppliers:
po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) doc = get_mapped_doc("Sales Order", source_name, {
if len(po) == 0: "Sales Order": {
doc = get_mapped_doc("Sales Order", source_name, { "doctype": "Purchase Order",
"Sales Order": { "field_no_map": [
"doctype": "Purchase Order", "address_display",
"field_no_map": [ "contact_display",
"address_display", "contact_mobile",
"contact_display", "contact_email",
"contact_mobile", "contact_person",
"contact_email", "taxes_and_charges",
"contact_person", "shipping_address",
"taxes_and_charges", "terms"
"shipping_address", ],
"terms" "validation": {
], "docstatus": ["=", 1]
"validation": {
"docstatus": ["=", 1]
}
},
"Sales Order Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_item"],
["parent", "sales_order"],
["stock_uom", "stock_uom"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
["delivery_date", "schedule_date"]
],
"field_no_map": [
"rate",
"price_list_rate",
"item_tax_template",
"discount_percentage",
"discount_amount",
"pricing_rules"
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
} }
}, target_doc, set_missing_values) },
"Sales Order Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_item"],
["parent", "sales_order"],
["stock_uom", "stock_uom"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
["delivery_date", "schedule_date"]
],
"field_no_map": [
"rate",
"price_list_rate",
"item_tax_template",
"discount_percentage",
"discount_amount",
"pricing_rules"
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
}
}, target_doc, set_missing_values)
doc.insert() doc.insert()
else:
suppliers =[]
if suppliers:
frappe.db.commit() frappe.db.commit()
return doc return doc
else:
frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
@frappe.whitelist() @frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None): def make_purchase_order(source_name, selected_items=None, target_doc=None):
@@ -1094,4 +1086,4 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
if not total_produced_qty and frappe.flags.in_patch: return if not total_produced_qty and frappe.flags.in_patch: return
frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)

View File

@@ -772,6 +772,59 @@ class TestSalesOrder(unittest.TestCase):
so.load_from_db() so.load_from_db()
so.cancel() so.cancel()
def test_drop_shipping_partial_order(self):
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
update_status as so_update_status
# make items
po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
so_items = [
{
"item_code": po_item1.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
},
{
"item_code": po_item2.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
# create so and po
so = make_sales_order(item_list=so_items, do_not_submit=True)
so.submit()
# create po for only one item
po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
po1.submit()
self.assertEqual(so.customer, po1.customer)
self.assertEqual(po1.items[0].sales_order, so.name)
self.assertEqual(po1.items[0].item_code, po_item1.item_code)
#test po item length
self.assertEqual(len(po1.items), 1)
# create po for remaining item
po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
po2.submit()
# teardown
so_update_status("Draft", so.name)
po1.cancel()
po2.cancel()
so.load_from_db()
so.cancel()
def test_reserved_qty_for_closing_so(self): def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"]) fields=["reserved_qty"])

View File

@@ -74,67 +74,71 @@ frappe.query_reports["Sales Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { 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; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Customer") {
row_values = data
if(tree_type == "Customer") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} else if (tree_type == "Item") { } else if (tree_type == "Item") {
row_values = data.slice(5,length-1).map(function (column) { row_values = data
return column.content; .slice(5, length - 1)
}) .map(function (column) {
} return column.content;
else { });
row_values = data.slice(3,length-1).map(function (column) { } else {
return column.content; row_values = data
}) .slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
}, 500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}) });
}, },
} }

View File

@@ -28,7 +28,7 @@ def delete_company_transactions(company_name):
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM", "Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
"Item Default"): "Item Default", "Customer", "Supplier"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@@ -8,26 +8,32 @@
"field_order": [ "field_order": [
"specification", "specification",
"value", "value",
"non_numeric",
"column_break_3", "column_break_3",
"min_value",
"max_value",
"formula_based_criteria",
"acceptance_formula" "acceptance_formula"
], ],
"fields": [ "fields": [
{ {
"fieldname": "specification", "fieldname": "specification",
"fieldtype": "Data", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Parameter", "label": "Parameter",
"oldfieldname": "specification", "oldfieldname": "specification",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Quality Inspection Parameter",
"print_width": "200px", "print_width": "200px",
"reqd": 1, "reqd": 1,
"width": "200px" "width": "100px"
}, },
{ {
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value", "fieldname": "value",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Acceptance Criteria", "label": "Acceptance Criteria Value",
"oldfieldname": "value", "oldfieldname": "value",
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
@@ -36,17 +42,45 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 &lt; 10</b>", "depends_on": "formula_based_criteria",
"description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula", "fieldname": "acceptance_formula",
"fieldtype": "Code", "fieldtype": "Code",
"in_list_view": 1,
"label": "Acceptance Criteria Formula" "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-16 16:33:42.421842", "modified": "2021-01-07 21:32:49.866439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Quality Inspection Parameter", "name": "Item Quality Inspection Parameter",

View File

@@ -4,6 +4,55 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", { 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) { refresh: function(frm) {
// Ignore cancellation of reference doctype on cancel all. // Ignore cancellation of reference doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; 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');

View File

@@ -136,6 +136,7 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "item_code.item_name",
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
@@ -143,6 +144,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "item_code.description",
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Description", "label": "Description",
@@ -236,7 +238,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-19 17:06:05.409963", "modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Quality Inspection", "name": "Quality Inspection",

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details import get_template_details
@@ -16,7 +16,7 @@ class QualityInspection(Document):
self.get_item_specification_details() self.get_item_specification_details()
if self.readings: if self.readings:
self.set_status_based_on_acceptance_formula() self.inspect_and_set_status()
def get_item_specification_details(self): def get_item_specification_details(self):
if not self.quality_inspection_template: if not self.quality_inspection_template:
@@ -29,9 +29,7 @@ class QualityInspection(Document):
parameters = get_template_details(self.quality_inspection_template) parameters = get_template_details(self.quality_inspection_template)
for d in parameters: for d in parameters:
child = self.append('readings', {}) child = self.append('readings', {})
child.specification = d.specification child.update(d)
child.value = d.value
child.acceptance_formula = d.acceptance_formula
child.status = "Accepted" child.status = "Accepted"
def get_quality_inspection_template(self): def get_quality_inspection_template(self):
@@ -69,35 +67,98 @@ class QualityInspection(Document):
doctype = 'Stock Entry Detail' doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name: 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(""" frappe.db.sql("""
UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 UPDATE
SET t1.quality_inspection = %s, t2.modified = %s `tab{child_doc}` t1, `tab{parent_doc}` t2
WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name SET
""".format(parent_doc=self.reference_type, child_doc=doctype), 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)) (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: 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 def set_status_based_on_acceptance_values(self, reading):
data = {} 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): for i in range(1, 11):
field = "reading_" + str(i) 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: return data
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"))
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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase):
qa.delete() qa.delete()
dn.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): 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", "specification": "Iron Content", # numeric reading
"formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", "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", "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
"reading_1": 0.7 "reading_1": "0.7"
}, },
{ {
"specification": "Mg Content", "specification": "Mg Content", # numeric reading
"acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9", "formula_based_criteria": 1,
"reading_1": 0.5, "acceptance_formula": "mean < 0.9",
"reading_2": 0.7, "reading_1": "0.5",
"reading_2": "0.7",
"reading_3": "random text" # check if random string input causes issues "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, 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[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Rejected") self.assertEqual(qa.readings[1].status, "Rejected")
self.assertEqual(qa.readings[2].status, "Accepted") self.assertEqual(qa.readings[2].status, "Accepted")
self.assertEqual(qa.readings[3].status, "Accepted")
qa.delete() qa.delete()
dn.delete() dn.delete()
@@ -86,11 +124,20 @@ def create_quality_inspection(**args):
qa.item_code = args.item_code or "_Test Item with QA" qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1 qa.sample_size = 1
qa.inspected_by = frappe.session.user 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): if isinstance(readings, list):
for entry in readings: for entry in readings:
create_quality_inspection_parameter(entry["specification"])
qa.append("readings", entry) qa.append("readings", entry)
else: else:
qa.append("readings", readings) qa.append("readings", readings)
@@ -101,3 +148,11 @@ def create_quality_inspection(**args):
qa.submit() qa.submit()
return qa 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()

View File

@@ -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) {
// }
});

View File

@@ -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
}

View File

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

View File

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

View File

@@ -7,21 +7,28 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"specification", "specification",
"value",
"status", "status",
"value",
"non_numeric",
"manual_inspection",
"column_break_4", "column_break_4",
"min_value",
"max_value",
"formula_based_criteria",
"acceptance_formula", "acceptance_formula",
"section_break_3", "section_break_3",
"reading_value",
"section_break_14",
"reading_1", "reading_1",
"reading_2", "reading_2",
"reading_3", "reading_3",
"column_break_10",
"reading_4", "reading_4",
"column_break_10",
"reading_5", "reading_5",
"reading_6", "reading_6",
"column_break_14",
"reading_7", "reading_7",
"reading_8", "reading_8",
"column_break_14",
"reading_9", "reading_9",
"reading_10" "reading_10"
], ],
@@ -29,19 +36,20 @@
{ {
"columns": 3, "columns": 3,
"fieldname": "specification", "fieldname": "specification",
"fieldtype": "Data", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Parameter", "label": "Parameter",
"oldfieldname": "specification", "oldfieldname": "specification",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Quality Inspection Parameter",
"reqd": 1 "reqd": 1
}, },
{ {
"columns": 2, "columns": 2,
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value", "fieldname": "value",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "label": "Acceptance Criteria Value",
"label": "Acceptance Criteria",
"oldfieldname": "value", "oldfieldname": "value",
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
@@ -67,7 +75,6 @@
"columns": 1, "columns": 1,
"fieldname": "reading_3", "fieldname": "reading_3",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Reading 3", "label": "Reading 3",
"oldfieldname": "reading_3", "oldfieldname": "reading_3",
"oldfieldtype": "Data" "oldfieldtype": "Data"
@@ -133,15 +140,18 @@
"options": "\nAccepted\nRejected" "options": "\nAccepted\nRejected"
}, },
{ {
"depends_on": "non_numeric",
"fieldname": "section_break_3", "fieldname": "section_break_3",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Value Based Inspection"
}, },
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 &lt; 10</b>", "depends_on": "formula_based_criteria",
"description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula", "fieldname": "acceptance_formula",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Acceptance Criteria Formula" "label": "Acceptance Criteria Formula"
@@ -153,12 +163,59 @@
{ {
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break" "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-04 18:16:53.978410", "modified": "2021-01-07 22:16:53.978410",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Quality Inspection Reading", "name": "Quality Inspection Reading",

View File

@@ -13,6 +13,7 @@ def get_template_details(template):
if not template: return [] if not template: return []
return frappe.get_all('Item Quality Inspection Parameter', 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}, filters={'parenttype': 'Quality Inspection Template', 'parent': template},
order_by="idx") order_by="idx")

View File

@@ -74,7 +74,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
update_party_blanket_order(args, out) 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): if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args)) out.update(get_pos_profile_item_details(args.company, args))