diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index dc23b2b2d05..89bb0184af8 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -69,6 +69,7 @@ class TestAccount(unittest.TestCase):
acc.account_name = "Accumulated Depreciation"
acc.parent_account = "Fixed Assets - _TC"
acc.company = "_Test Company"
+ acc.account_type = "Accumulated Depreciation"
acc.insert()
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
@@ -149,7 +150,7 @@ def _make_test_records(verbose):
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
- ["_Test Accumulated Depreciations", "Current Assets", 0, None, None],
+ ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index fa4d40e297b..e1b331be2b3 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -152,10 +152,9 @@ def build_forest(data):
return [parent_account]
elif account_name == child:
parent_account_list = return_parent(data, parent_account)
- if not parent_account_list:
+ if not parent_account_list and parent_account:
frappe.throw(_("The parent account {0} does not exists in the uploaded template").format(
frappe.bold(parent_account)))
-
return [child] + parent_account_list
charts_map, paths = {}, []
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 100bb1d3e38..b358f56671f 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -330,9 +330,9 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
if pr_doc.mixed_conditions:
amt = args.get('qty') * args.get("price_list_rate")
if args.get("item_code") != row.get("item_code"):
- amt = row.get('qty') * (row.get("price_list_rate") or args.get("rate"))
+ amt = flt(row.get('qty')) * flt(row.get("price_list_rate") or args.get("rate"))
- sum_qty += row.get("stock_qty") or args.get("stock_qty") or args.get("qty")
+ sum_qty += flt(row.get("stock_qty")) or flt(args.get("stock_qty")) or flt(args.get("qty"))
sum_amt += amt
if pr_doc.is_cumulative:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index c5c54837a7b..9292b633fc3 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -174,7 +174,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
read_only: 0,
fieldtype:'Date',
label: __('Release Date'),
- default: me.frm.doc.release_date
+ default: me.frm.doc.release_date,
+ reqd: 1
},
{
fieldname: 'hold_comment',
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 3cd988ccd23..0e0945454c9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -13,23 +13,18 @@
"supplier_name",
"tax_id",
"due_date",
- "is_paid",
- "is_return",
- "apply_tds",
"column_break1",
"company",
"posting_date",
"posting_time",
"set_posting_time",
+ "is_paid",
+ "is_return",
+ "apply_tds",
"amended_from",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
- "sb_14",
- "on_hold",
- "release_date",
- "cb_17",
- "hold_comment",
"supplier_invoice_details",
"bill_no",
"column_break_15",
@@ -73,9 +68,9 @@
"base_total",
"base_net_total",
"column_break_28",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_49",
@@ -137,10 +132,15 @@
"terms",
"printing_settings",
"letter_head",
- "group_same_items",
- "column_break_112",
"select_print_heading",
+ "column_break_112",
+ "group_same_items",
"language",
+ "sb_14",
+ "on_hold",
+ "release_date",
+ "cb_17",
+ "hold_comment",
"more_info",
"credit_to",
"party_account_currency",
@@ -190,6 +190,7 @@
"oldfieldtype": "Link",
"options": "Supplier",
"print_hide": 1,
+ "reqd": 1,
"search_index": 1
},
{
@@ -1232,6 +1233,7 @@
"print_hide": 1
},
{
+ "collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section",
@@ -1298,7 +1300,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-30 19:13:49.610538",
+ "modified": "2020-04-17 13:05:25.199832",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index db3f72ada08..52a5be09843 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -12,15 +12,11 @@
"item_name",
"description_section",
"description",
- "item_group",
"brand",
- "image_section",
+ "col_break7",
+ "item_group",
"image",
"image_view",
- "manufacture_details",
- "manufacturer",
- "column_break_13",
- "manufacturer_part_no",
"quantity_and_rate",
"received_qty",
"qty",
@@ -55,20 +51,19 @@
"item_tax_amount",
"landed_cost_voucher_amount",
"rm_supp_cost",
- "item_weight_details",
- "weight_per_unit",
- "total_weight",
- "column_break_38",
- "weight_uom",
"warehouse_section",
"warehouse",
- "rejected_warehouse",
"from_warehouse",
"quality_inspection",
- "batch_no",
- "col_br_wh",
"serial_no",
+ "col_br_wh",
+ "rejected_warehouse",
+ "batch_no",
"rejected_serial_no",
+ "manufacture_details",
+ "manufacturer",
+ "column_break_13",
+ "manufacturer_part_no",
"accounting",
"expense_account",
"col_break5",
@@ -92,6 +87,11 @@
"po_detail",
"purchase_receipt",
"pr_detail",
+ "item_weight_details",
+ "weight_per_unit",
+ "total_weight",
+ "column_break_38",
+ "weight_uom",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -550,23 +550,21 @@
},
{
"fieldname": "brand",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Brand",
- "oldfieldname": "brand",
- "oldfieldtype": "Data",
- "print_hide": 1
- },
- {
- "fieldname": "item_group",
"fieldtype": "Link",
"hidden": 1,
- "label": "Item Group",
- "oldfieldname": "item_group",
- "oldfieldtype": "Link",
- "options": "Item Group",
+ "label": "Brand",
"print_hide": 1,
- "read_only": 1
+ "options": "Brand"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fetch_if_empty": 1,
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "print_hide": 1,
+ "read_only": 1,
+ "options": "Item Group"
},
{
"description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges",
@@ -720,12 +718,6 @@
"fieldname": "section_break_82",
"fieldtype": "Section Break"
},
- {
- "collapsible": 1,
- "fieldname": "image_section",
- "fieldtype": "Section Break",
- "label": "Image"
- },
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
@@ -737,6 +729,7 @@
"fieldtype": "Column Break"
},
{
+ "collapsible": 1,
"fieldname": "manufacture_details",
"fieldtype": "Section Break",
"label": "Manufacture"
@@ -771,12 +764,17 @@
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"options": "Warehouse"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "col_break7",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-07 18:34:35.104178",
+ "modified": "2020-04-22 10:37:35.103176",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -784,4 +782,4 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e239f9143dc..918fa140b2d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:05",
@@ -74,9 +75,9 @@
"base_total",
"base_net_total",
"column_break_32",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"taxes_and_charges",
"column_break_38",
@@ -1577,7 +1578,8 @@
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
- "modified": "2020-02-10 04:57:11.221180",
+ "links": [],
+ "modified": "2020-04-17 12:38:41.435728",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 0e54b62caa4..a2819af5086 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1926,16 +1926,6 @@ class TestSalesInvoice(unittest.TestCase):
item.taxes = []
item.save()
- def test_customer_provided_parts_si(self):
- create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- si = create_sales_invoice(item_code='CUST-0987', rate=0)
- self.assertEqual(si.get("items")[0].allow_zero_valuation_rate, 1)
- self.assertEqual(si.get("items")[0].amount, 0)
-
- # test if Sales Invoice with rate is allowed
- si2 = create_sales_invoice(item_code='CUST-0987', do_not_save=True)
- self.assertRaises(frappe.ValidationError, si2.save)
-
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index 9a2205a5791..378fa3791c1 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -2,7 +2,7 @@
', {therapy:y[0],
+ name: y[1], encounter:y[2], practitioner:y[3], date:y[4],
+ department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field);
+
+ row.find("a").click(function() {
+ frm.doc.therapy_type = $(this).attr("data-therapy");
+ frm.doc.practitioner = $(this).attr("data-practitioner");
+ frm.doc.department = $(this).attr("data-department");
+ frm.doc.therapy_plan = $(this).attr("data-therapy-plan");
+ frm.refresh_field("therapy_type");
+ frm.refresh_field("practitioner");
+ frm.refresh_field("department");
+ frm.refresh_field("therapy-plan");
+ frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {
+ if (r.default_duration) {
+ frm.set_value('duration', r.default_duration)
+ }
+ });
+ d.hide();
+ return false;
+ });
+ });
+ d.show();
+};
+
let create_vital_signs = function(frm) {
if (!frm.doc.patient) {
frappe.throw(__('Please select patient'));
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 81f75975636..9297588509c 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -24,6 +24,10 @@
"column_break_13",
"procedure_template",
"procedure_prescription",
+ "therapy_type",
+ "get_prescribed_therapies",
+ "therapy_plan",
+ "service_unit",
"section_break_12",
"practitioner",
"department",
@@ -271,6 +275,28 @@
"print_hide": 1,
"report_hide": 1
},
+ {
+ "depends_on": "eval:doc.patient;",
+ "fieldname": "therapy_type",
+ "fieldtype": "Link",
+ "label": "Therapy",
+ "options": "Therapy Type",
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval:doc.patient && doc.__islocal;",
+ "fieldname": "get_prescribed_therapies",
+ "fieldtype": "Button",
+ "label": "Get Prescribed Therapies"
+ },
+ {
+ "depends_on": "eval: doc.patient && doc.therapy_type",
+ "fieldname": "therapy_plan",
+ "fieldtype": "Link",
+ "label": "Therapy Plan",
+ "mandatory_depends_on": "eval: doc.patient && doc.therapy_type",
+ "options": "Therapy Plan"
+ },
{
"fieldname": "ref_sales_invoice",
"fieldtype": "Link",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index fcd87d719b8..67fd82dbc2d 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -415,11 +415,36 @@ def get_events(start, end, filters=None):
@frappe.whitelist()
def get_procedure_prescribed(patient):
- return frappe.db.sql("""select pp.name, pp.procedure, pp.parent, ct.practitioner,
- ct.encounter_date, pp.practitioner, pp.date, pp.department
- from `tabPatient Encounter` ct, `tabProcedure Prescription` pp
- where ct.patient=%(patient)s and pp.parent=ct.name and pp.appointment_booked=0
- order by ct.creation desc""", {'patient': patient})
+ return frappe.db.sql(
+ """
+ SELECT
+ pp.name, pp.procedure, pp.parent, ct.practitioner,
+ ct.encounter_date, pp.practitioner, pp.date, pp.department
+ FROM
+ `tabPatient Encounter` ct, `tabProcedure Prescription` pp
+ WHERE
+ ct.patient=%(patient)s and pp.parent=ct.name and pp.appointment_booked=0
+ ORDER BY
+ ct.creation desc
+ """, {'patient': patient}
+ )
+
+
+@frappe.whitelist()
+def get_prescribed_therapies(patient):
+ return frappe.db.sql(
+ """
+ SELECT
+ t.therapy_type, t.name, t.parent, e.practitioner,
+ e.encounter_date, e.therapy_plan, e.visit_department
+ FROM
+ `tabPatient Encounter` e, `tabTherapy Plan Detail` t
+ WHERE
+ e.patient=%(patient)s and t.parent=e.name
+ ORDER BY
+ e.creation desc
+ """, {'patient': patient}
+ )
def update_appointment_status():
diff --git a/erpnext/healthcare/doctype/patient_assessment/__init__.py b/erpnext/healthcare/doctype/patient_assessment/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js
new file mode 100644
index 00000000000..c7074e88d5d
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js
@@ -0,0 +1,86 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Patient Assessment', {
+ refresh: function(frm) {
+ if (frm.doc.assessment_template) {
+ frm.trigger('set_score_range');
+ }
+
+ if (!frm.doc.__islocal) {
+ frm.trigger('show_patient_progress');
+ }
+ },
+
+ assessment_template: function(frm) {
+ if (frm.doc.assessment_template) {
+ frappe.call({
+ 'method': 'frappe.client.get',
+ args: {
+ doctype: 'Patient Assessment Template',
+ name: frm.doc.assessment_template
+ },
+ callback: function(data) {
+ frm.doc.assessment_sheet = [];
+ $.each(data.message.parameters, function(_i, e) {
+ let entry = frm.add_child('assessment_sheet');
+ entry.parameter = e.assessment_parameter;
+ });
+
+ frm.set_value('scale_min', data.message.scale_min);
+ frm.set_value('scale_max', data.message.scale_max);
+ frm.set_value('assessment_description', data.message.assessment_description);
+ frm.set_value('total_score', data.message.scale_max * data.message.parameters.length);
+ frm.trigger('set_score_range');
+ refresh_field('assessment_sheet');
+ }
+ });
+ }
+ },
+
+ set_score_range: function(frm) {
+ let options = [];
+ for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) {
+ options.push(i);
+ }
+ frappe.meta.get_docfield('Patient Assessment Sheet', 'score', frm.doc.name).options = [''].concat(options);
+ },
+
+ calculate_total_score: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ let total_score = 0;
+ $.each(frm.doc.assessment_sheet || [], function(_i, item) {
+ if (item.score) {
+ total_score += parseInt(item.score);
+ }
+ });
+
+ frm.set_value('total_score_obtained', total_score);
+ },
+
+ show_patient_progress: function(frm) {
+ let bars = [];
+ let message = '';
+ let added_min = false;
+
+ let title = __('{0} out of {1}', [frm.doc.total_score_obtained, frm.doc.total_score]);
+
+ bars.push({
+ 'title': title,
+ 'width': (frm.doc.total_score_obtained / frm.doc.total_score * 100) + '%',
+ 'progress_class': 'progress-bar-success'
+ });
+ if (bars[0].width == '0%') {
+ bars[0].width = '0.5%';
+ added_min = 0.5;
+ }
+ message = title;
+ frm.dashboard.add_progress(__('Status'), bars, message);
+ },
+});
+
+frappe.ui.form.on('Patient Assessment Sheet', {
+ score: function(frm, cdt, cdn) {
+ frm.events.calculate_total_score(frm, cdt, cdn);
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json
new file mode 100644
index 00000000000..3952a8153f7
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json
@@ -0,0 +1,172 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2020-04-19 22:45:12.356209",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "therapy_session",
+ "patient",
+ "assessment_template",
+ "column_break_4",
+ "healthcare_practitioner",
+ "assessment_datetime",
+ "assessment_description",
+ "section_break_7",
+ "assessment_sheet",
+ "section_break_9",
+ "total_score_obtained",
+ "column_break_11",
+ "total_score",
+ "scale_min",
+ "scale_max",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fetch_from": "therapy_session.patient",
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Patient",
+ "options": "Patient",
+ "reqd": 1
+ },
+ {
+ "fieldname": "assessment_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Assessment Template",
+ "options": "Patient Assessment Template",
+ "reqd": 1
+ },
+ {
+ "fieldname": "therapy_session",
+ "fieldtype": "Link",
+ "label": "Therapy Session",
+ "options": "Therapy Session"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "therapy_session.practitioner",
+ "fieldname": "healthcare_practitioner",
+ "fieldtype": "Link",
+ "label": "Healthcare Practitioner",
+ "options": "Healthcare Practitioner"
+ },
+ {
+ "fieldname": "assessment_datetime",
+ "fieldtype": "Datetime",
+ "label": "Assessment Datetime"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "assessment_sheet",
+ "fieldtype": "Table",
+ "label": "Assessment Sheet",
+ "options": "Patient Assessment Sheet"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "total_score",
+ "fieldtype": "Int",
+ "label": "Total Score",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total_score_obtained",
+ "fieldtype": "Int",
+ "label": "Total Score Obtained",
+ "read_only": 1
+ },
+ {
+ "fieldname": "scale_min",
+ "fieldtype": "Int",
+ "hidden": 1,
+ "label": "Scale Min",
+ "read_only": 1
+ },
+ {
+ "fieldname": "scale_max",
+ "fieldtype": "Int",
+ "hidden": 1,
+ "label": "Scale Max",
+ "read_only": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "HLC-PA-.YYYY.-"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Patient Assessment",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "assessment_description",
+ "fieldtype": "Small Text",
+ "label": "Assessment Description"
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-04-21 13:23:09.815007",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient Assessment",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Physician",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "patient",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py
new file mode 100644
index 00000000000..3033a3e6ac9
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py
@@ -0,0 +1,36 @@
+# -*- 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
+from frappe.model.mapper import get_mapped_doc
+
+class PatientAssessment(Document):
+ def validate(self):
+ self.set_total_score()
+
+ def set_total_score(self):
+ total_score = 0
+ for entry in self.assessment_sheet:
+ total_score += int(entry.score)
+ self.total_score_obtained = total_score
+
+@frappe.whitelist()
+def create_patient_assessment(source_name, target_doc=None):
+ doc = get_mapped_doc('Therapy Session', source_name, {
+ 'Therapy Session': {
+ 'doctype': 'Patient Assessment',
+ 'field_map': [
+ ['therapy_session', 'name'],
+ ['patient', 'patient'],
+ ['practitioner', 'practitioner']
+ ]
+ }
+ }, target_doc)
+
+ return doc
+
+
+
diff --git a/erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py b/erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py
new file mode 100644
index 00000000000..3fda8550f6c
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py
@@ -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 TestPatientAssessment(unittest.TestCase):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment_detail/__init__.py b/erpnext/healthcare/doctype/patient_assessment_detail/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json
new file mode 100644
index 00000000000..179f441044e
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json
@@ -0,0 +1,32 @@
+{
+ "actions": [],
+ "creation": "2020-04-19 19:33:00.115395",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "assessment_parameter"
+ ],
+ "fields": [
+ {
+ "fieldname": "assessment_parameter",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Assessment Parameter",
+ "options": "Patient Assessment Parameter",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-04-19 19:33:00.115395",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient Assessment Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py
new file mode 100644
index 00000000000..0519599ac0c
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py
@@ -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 PatientAssessmentDetail(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/__init__.py b/erpnext/healthcare/doctype/patient_assessment_parameter/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js
new file mode 100644
index 00000000000..2c5d270d575
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Patient Assessment Parameter', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json
new file mode 100644
index 00000000000..098bdefea70
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json
@@ -0,0 +1,45 @@
+{
+ "actions": [],
+ "autoname": "field:assessment_parameter",
+ "creation": "2020-04-15 14:34:46.551042",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "assessment_parameter"
+ ],
+ "fields": [
+ {
+ "fieldname": "assessment_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Assessment Parameter",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-20 09:22:19.135196",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient Assessment 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
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py
new file mode 100644
index 00000000000..b8e00747171
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py
@@ -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 PatientAssessmentParameter(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py b/erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py
new file mode 100644
index 00000000000..e722f9905ec
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py
@@ -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 TestPatientAssessmentParameter(unittest.TestCase):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment_sheet/__init__.py b/erpnext/healthcare/doctype/patient_assessment_sheet/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json
new file mode 100644
index 00000000000..64e4aef7cf0
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json
@@ -0,0 +1,57 @@
+{
+ "actions": [],
+ "creation": "2020-04-19 23:07:02.220244",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "parameter",
+ "score",
+ "time",
+ "column_break_4",
+ "comments"
+ ],
+ "fields": [
+ {
+ "fieldname": "parameter",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Parameter",
+ "options": "Patient Assessment Parameter",
+ "reqd": 1
+ },
+ {
+ "fieldname": "score",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Score",
+ "reqd": 1
+ },
+ {
+ "fieldname": "time",
+ "fieldtype": "Time",
+ "label": "Time"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "comments",
+ "fieldtype": "Small Text",
+ "label": "Comments"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-04-20 09:56:28.746619",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient Assessment Sheet",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py
new file mode 100644
index 00000000000..40da7630132
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py
@@ -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 PatientAssessmentSheet(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment_template/__init__.py b/erpnext/healthcare/doctype/patient_assessment_template/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js
new file mode 100644
index 00000000000..40419362a4a
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Patient Assessment Template', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json
new file mode 100644
index 00000000000..de006b18056
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json
@@ -0,0 +1,109 @@
+{
+ "actions": [],
+ "autoname": "field:assessment_name",
+ "creation": "2020-04-19 19:33:13.204707",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "assessment_name",
+ "section_break_2",
+ "parameters",
+ "assessment_scale_details_section",
+ "scale_min",
+ "scale_max",
+ "column_break_8",
+ "assessment_description"
+ ],
+ "fields": [
+ {
+ "fieldname": "parameters",
+ "fieldtype": "Table",
+ "label": "Parameters",
+ "options": "Patient Assessment Detail"
+ },
+ {
+ "fieldname": "assessment_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Assessment Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break",
+ "label": "Assessment Parameters"
+ },
+ {
+ "fieldname": "assessment_scale_details_section",
+ "fieldtype": "Section Break",
+ "label": "Assessment Scale"
+ },
+ {
+ "fieldname": "scale_min",
+ "fieldtype": "Int",
+ "label": "Scale Minimum"
+ },
+ {
+ "fieldname": "scale_max",
+ "fieldtype": "Int",
+ "label": "Scale Maximum"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "assessment_description",
+ "fieldtype": "Small Text",
+ "label": "Assessment Description"
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-21 13:14:19.075167",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient Assessment Template",
+ "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": "Healthcare Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Physician",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py
new file mode 100644
index 00000000000..083cab5d017
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py
@@ -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 PatientAssessmentTemplate(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py b/erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py
new file mode 100644
index 00000000000..86dbd5438cd
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py
@@ -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 TestPatientAssessmentTemplate(unittest.TestCase):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
index 0e34164d07d..7600900495f 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
@@ -3,6 +3,10 @@
frappe.ui.form.on('Patient Encounter', {
setup: function(frm) {
+ frm.get_field('therapies').grid.editable_fields = [
+ {fieldname: 'therapy_type', columns: 8},
+ {fieldname: 'no_of_sessions', columns: 2}
+ ];
frm.get_field('drug_prescription').grid.editable_fields = [
{fieldname: 'drug_code', columns: 2},
{fieldname: 'drug_name', columns: 2},
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json
index 935935e0c01..12203fde8d7 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json
@@ -42,6 +42,10 @@
"lab_test_prescription",
"sb_procedures",
"procedure_prescription",
+ "rehabilitation_section",
+ "therapy_plan",
+ "therapies",
+ "section_break_33",
"encounter_comment",
"sb_refs",
"company",
@@ -255,6 +259,29 @@
"print_hide": 1,
"read_only": 1
},
+ {
+ "fieldname": "rehabilitation_section",
+ "fieldtype": "Section Break",
+ "label": "Rehabilitation"
+ },
+ {
+ "fieldname": "therapies",
+ "fieldtype": "Table",
+ "label": "Therapies",
+ "options": "Therapy Plan Detail"
+ },
+ {
+ "fieldname": "section_break_33",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "therapy_plan",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Therapy Plan",
+ "options": "Therapy Plan",
+ "read_only": 1
+ },
{
"fieldname": "appointment_type",
"fieldtype": "Link",
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
index ade4748ece1..767643bc73a 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr
from frappe import _
@@ -22,6 +23,24 @@ class PatientEncounter(Document):
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
delete_medical_record(self)
+ def on_submit(self):
+ create_therapy_plan(self)
+
+def create_therapy_plan(encounter):
+ if len(encounter.therapies):
+ doc = frappe.new_doc('Therapy Plan')
+ doc.patient = encounter.patient
+ doc.start_date = encounter.encounter_date
+ for entry in encounter.therapies:
+ doc.append('therapy_plan_details', {
+ 'therapy_type': entry.therapy_type,
+ 'no_of_sessions': entry.no_of_sessions
+ })
+ doc.save(ignore_permissions=True)
+ if doc.get('name'):
+ encounter.db_set('therapy_plan', doc.name)
+ frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
+
def insert_encounter_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
diff --git a/erpnext/healthcare/doctype/therapy_plan/__init__.py b/erpnext/healthcare/doctype/therapy_plan/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
new file mode 100644
index 00000000000..526bb95b70e
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import getdate
+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
+from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient
+
+class TestTherapyPlan(unittest.TestCase):
+ def test_creation_on_encounter_submission(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ encounter = create_encounter(patient, medical_department, practitioner)
+ self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan))
+
+ def test_status(self):
+ plan = create_therapy_plan()
+ self.assertEquals(plan.status, 'Not Started')
+
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab')
+ frappe.get_doc(session).submit()
+ self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
+
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab')
+ frappe.get_doc(session).submit()
+ self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
+
+
+def create_therapy_plan():
+ patient = create_patient()
+ therapy_type = create_therapy_type()
+ plan = frappe.new_doc('Therapy Plan')
+ plan.patient = patient
+ plan.start_date = getdate()
+ plan.append('therapy_plan_details', {
+ 'therapy_type': therapy_type.name,
+ 'no_of_sessions': 2
+ })
+ plan.save()
+ return plan
+
+def create_encounter(patient, medical_department, practitioner):
+ encounter = frappe.new_doc('Patient Encounter')
+ encounter.patient = patient
+ encounter.practitioner = practitioner
+ encounter.medical_department = medical_department
+ therapy_type = create_therapy_type()
+ encounter.append('therapies', {
+ 'therapy_type': therapy_type.name,
+ 'no_of_sessions': 2
+ })
+ encounter.save()
+ encounter.submit()
+ return encounter
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
new file mode 100644
index 00000000000..dea0cfeb849
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
@@ -0,0 +1,90 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Therapy Plan', {
+ setup: function(frm) {
+ frm.get_field('therapy_plan_details').grid.editable_fields = [
+ {fieldname: 'therapy_type', columns: 6},
+ {fieldname: 'no_of_sessions', columns: 2},
+ {fieldname: 'sessions_completed', columns: 2}
+ ];
+ },
+
+ refresh: function(frm) {
+ if (!frm.doc.__islocal) {
+ frm.trigger('show_progress_for_therapies');
+ }
+
+ if (!frm.doc.__islocal && frm.doc.status != 'Completed') {
+ let therapy_types = (frm.doc.therapy_plan_details || []).map(function(d){ return d.therapy_type });
+ const fields = [{
+ fieldtype: 'Link',
+ label: __('Therapy Type'),
+ fieldname: 'therapy_type',
+ options: 'Therapy Type',
+ reqd: 1,
+ get_query: function() {
+ return {
+ filters: { 'therapy_type': ['in', therapy_types]}
+ }
+ }
+ }];
+
+ frm.add_custom_button(__('Therapy Session'), function() {
+ frappe.prompt(fields, data => {
+ frappe.call({
+ method: 'erpnext.healthcare.doctype.therapy_plan.therapy_plan.make_therapy_session',
+ args: {
+ therapy_plan: frm.doc.name,
+ patient: frm.doc.patient,
+ therapy_type: data.therapy_type
+ },
+ freeze: true,
+ callback: function(r) {
+ if (r.message) {
+ frappe.model.sync(r.message);
+ frappe.set_route('Form', r.message.doctype, r.message.name);
+ }
+ }
+ });
+ }, __('Select Therapy Type'), __('Create'));
+ }, __('Create'));
+ }
+ },
+
+ show_progress_for_therapies: function(frm) {
+ let bars = [];
+ let message = '';
+ let added_min = false;
+
+ // completed sessions
+ let title = __('{0} sessions completed', [frm.doc.total_sessions_completed]);
+ if (frm.doc.total_sessions_completed === 1) {
+ title = __('{0} session completed', [frm.doc.total_sessions_completed]);
+ }
+ title += __(' out of {0}', [frm.doc.total_sessions]);
+
+ bars.push({
+ 'title': title,
+ 'width': (frm.doc.total_sessions_completed / frm.doc.total_sessions * 100) + '%',
+ 'progress_class': 'progress-bar-success'
+ });
+ if (bars[0].width == '0%') {
+ bars[0].width = '0.5%';
+ added_min = 0.5;
+ }
+ message = title;
+ frm.dashboard.add_progress(__('Status'), bars, message);
+ },
+});
+
+frappe.ui.form.on('Therapy Plan Detail', {
+ no_of_sessions: function(frm) {
+ let total = 0;
+ $.each(frm.doc.therapy_plan_details, function(_i, e) {
+ total += e.no_of_sessions;
+ });
+ frm.set_value('total_sessions', total);
+ refresh_field('total_sessions');
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json
new file mode 100644
index 00000000000..ca78b6618ec
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json
@@ -0,0 +1,151 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2020-03-29 20:56:49.758602",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "patient",
+ "patient_name",
+ "column_break_4",
+ "status",
+ "start_date",
+ "section_break_3",
+ "therapy_plan_details",
+ "title",
+ "section_break_9",
+ "total_sessions",
+ "column_break_11",
+ "total_sessions_completed"
+ ],
+ "fields": [
+ {
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Patient",
+ "options": "Patient",
+ "reqd": 1
+ },
+ {
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Start Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "therapy_plan_details",
+ "fieldtype": "Table",
+ "label": "Therapy Plan Details",
+ "options": "Therapy Plan Detail",
+ "reqd": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "HLC-THP-.YYYY.-"
+ },
+ {
+ "fetch_from": "patient.patient_name",
+ "fieldname": "patient_name",
+ "fieldtype": "Data",
+ "label": "Patient Name",
+ "read_only": 1
+ },
+ {
+ "default": "{patient_name}",
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "no_copy": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "total_sessions",
+ "fieldtype": "Int",
+ "label": "Total Sessions",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total_sessions_completed",
+ "fieldtype": "Int",
+ "label": "Total Sessions Completed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "Not Started\nIn Progress\nCompleted\nCancelled",
+ "read_only": 1
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-21 13:13:43.956014",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Therapy Plan",
+ "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": "Healthcare Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Physician",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "search_fields": "patient",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "patient",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
new file mode 100644
index 00000000000..201264f8294
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -0,0 +1,42 @@
+# -*- 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 TherapyPlan(Document):
+ def validate(self):
+ self.set_totals()
+ self.set_status()
+
+ def set_status(self):
+ if not self.total_sessions_completed:
+ self.status = 'Not Started'
+ else:
+ if self.total_sessions_completed < self.total_sessions:
+ self.status = 'In Progress'
+ elif self.total_sessions_completed == self.total_sessions:
+ self.status = 'Completed'
+
+ def set_totals(self):
+ total_sessions = sum([int(d.no_of_sessions) for d in self.get('therapy_plan_details')])
+ total_sessions_completed = sum([int(d.sessions_completed) for d in self.get('therapy_plan_details')])
+ self.db_set('total_sessions', total_sessions)
+ self.db_set('total_sessions_completed', total_sessions_completed)
+
+
+@frappe.whitelist()
+def make_therapy_session(therapy_plan, patient, therapy_type):
+ therapy_type = frappe.get_doc('Therapy Type', therapy_type)
+
+ therapy_session = frappe.new_doc('Therapy Session')
+ therapy_session.therapy_plan = therapy_plan
+ therapy_session.patient = patient
+ therapy_session.therapy_type = therapy_type.name
+ therapy_session.duration = therapy_type.default_duration
+ therapy_session.rate = therapy_type.rate
+ therapy_session.exercises = therapy_type.exercises
+
+ return therapy_session.as_dict()
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py
new file mode 100644
index 00000000000..df647829dbc
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'therapy_plan',
+ 'transactions': [
+ {
+ 'label': _('Therapy Sessions'),
+ 'items': ['Therapy Session']
+ }
+ ]
+ }
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js
new file mode 100644
index 00000000000..63967aff33b
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js
@@ -0,0 +1,11 @@
+frappe.listview_settings['Therapy Plan'] = {
+ get_indicator: function(doc) {
+ var colors = {
+ 'Completed': 'green',
+ 'In Progress': 'orange',
+ 'Not Started': 'red',
+ 'Cancelled': 'grey'
+ };
+ return [__(doc.status), colors[doc.status], 'status,=,' + doc.status];
+ }
+};
diff --git a/erpnext/healthcare/doctype/therapy_plan_detail/__init__.py b/erpnext/healthcare/doctype/therapy_plan_detail/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json
new file mode 100644
index 00000000000..9eb20e2ef3b
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json
@@ -0,0 +1,48 @@
+{
+ "actions": [],
+ "creation": "2020-03-29 20:52:57.068731",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "therapy_type",
+ "no_of_sessions",
+ "sessions_completed"
+ ],
+ "fields": [
+ {
+ "fieldname": "therapy_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Therapy Type",
+ "options": "Therapy Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "no_of_sessions",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "No of Sessions"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.parenttype=='Therapy Plan';",
+ "fieldname": "sessions_completed",
+ "fieldtype": "Int",
+ "label": "Sessions Completed",
+ "read_only": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-03-30 22:02:01.740109",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Therapy Plan Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py
new file mode 100644
index 00000000000..44211f32e3a
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py
@@ -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 TherapyPlanDetail(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/therapy_session/__init__.py b/erpnext/healthcare/doctype/therapy_session/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/therapy_session/test_therapy_session.py b/erpnext/healthcare/doctype/therapy_session/test_therapy_session.py
new file mode 100644
index 00000000000..75bb8df1963
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_session/test_therapy_session.py
@@ -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 TestTherapySession(unittest.TestCase):
+ pass
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
new file mode 100644
index 00000000000..bb675752bbc
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
@@ -0,0 +1,60 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Therapy Session', {
+ setup: function(frm) {
+ frm.get_field('exercises').grid.editable_fields = [
+ {fieldname: 'exercise_type', columns: 7},
+ {fieldname: 'counts_target', columns: 1},
+ {fieldname: 'counts_completed', columns: 1},
+ {fieldname: 'assistance_level', columns: 1}
+ ];
+ },
+
+ refresh: function(frm) {
+ if (!frm.doc.__islocal) {
+ let target = 0;
+ let completed = 0;
+ $.each(frm.doc.exercises, function(_i, e) {
+ target += e.counts_target;
+ completed += e.counts_completed;
+ });
+ frm.dashboard.add_indicator(__('Counts Targetted: {0}', [target]), 'blue');
+ frm.dashboard.add_indicator(__('Counts Completed: {0}', [completed]), (completed < target) ? 'orange' : 'green');
+ }
+
+ if (frm.doc.docstatus === 1) {
+ frm.add_custom_button(__('Patient Assessment'),function() {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.healthcare.doctype.patient_assessment.patient_assessment.create_patient_assessment',
+ frm: frm,
+ })
+ }, 'Create');
+ }
+ },
+
+ therapy_type: function(frm) {
+ if (frm.doc.therapy_type) {
+ frappe.call({
+ 'method': 'frappe.client.get',
+ args: {
+ doctype: 'Therapy Type',
+ name: frm.doc.therapy_type
+ },
+ callback: function(data) {
+ frm.set_value('duration', data.message.default_duration);
+ frm.set_value('rate', data.message.rate);
+ frm.doc.exercises = [];
+ $.each(data.message.exercises, function(_i, e) {
+ let exercise = frm.add_child('exercises');
+ exercise.exercise_type = e.exercise_type;
+ exercise.difficulty_level = e.difficulty_level;
+ exercise.counts_target = e.counts_target;
+ exercise.assistance_level = e.assistance_level;
+ });
+ refresh_field('exercises');
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json
new file mode 100644
index 00000000000..5ff719672fb
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json
@@ -0,0 +1,218 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2020-03-11 08:57:40.669857",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "appointment",
+ "patient",
+ "patient_age",
+ "gender",
+ "column_break_5",
+ "therapy_plan",
+ "therapy_type",
+ "practitioner",
+ "department",
+ "details_section",
+ "duration",
+ "rate",
+ "location",
+ "company",
+ "column_break_12",
+ "service_unit",
+ "start_date",
+ "start_time",
+ "invoiced",
+ "exercises_section",
+ "exercises",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "options": "HLC-THP-.YYYY.-"
+ },
+ {
+ "fieldname": "appointment",
+ "fieldtype": "Link",
+ "label": "Appointment",
+ "options": "Patient Appointment"
+ },
+ {
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Patient",
+ "options": "Patient",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "patient.sex",
+ "fieldname": "gender",
+ "fieldtype": "Link",
+ "label": "Gender",
+ "options": "Gender",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "practitioner",
+ "fieldtype": "Link",
+ "label": "Healthcare Practitioner",
+ "options": "Healthcare Practitioner"
+ },
+ {
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "therapy_template.default_duration",
+ "fieldname": "duration",
+ "fieldtype": "Int",
+ "label": "Duration"
+ },
+ {
+ "fieldname": "location",
+ "fieldtype": "Select",
+ "label": "Location",
+ "options": "\nCenter\nHome\nTele"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "therapy_template.rate",
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "label": "Rate"
+ },
+ {
+ "fieldname": "exercises_section",
+ "fieldtype": "Section Break",
+ "label": "Exercises"
+ },
+ {
+ "fieldname": "exercises",
+ "fieldtype": "Table",
+ "label": "Exercises",
+ "options": "Exercise"
+ },
+ {
+ "depends_on": "eval: doc.therapy_plan",
+ "fieldname": "therapy_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Therapy Type",
+ "options": "Therapy Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "therapy_plan",
+ "fieldtype": "Link",
+ "label": "Therapy Plan",
+ "options": "Therapy Plan",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Therapy Session",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "service_unit",
+ "fieldtype": "Link",
+ "label": "Healthcare Service Unit",
+ "options": "Healthcare Service Unit"
+ },
+ {
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Start Date"
+ },
+ {
+ "fieldname": "start_time",
+ "fieldtype": "Time",
+ "label": "Start Time"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "default": "0",
+ "fieldname": "invoiced",
+ "fieldtype": "Check",
+ "label": "Invoiced",
+ "read_only": 1
+ },
+ {
+ "fieldname": "patient_age",
+ "fieldtype": "Data",
+ "label": "Patient Age",
+ "read_only": 1
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-04-21 13:16:46.378798",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Therapy Session",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Physician",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "search_fields": "patient,appointment,therapy_plan,therapy_type",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "patient",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
new file mode 100644
index 00000000000..45d2ee60e61
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
@@ -0,0 +1,55 @@
+# -*- 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
+from frappe.model.mapper import get_mapped_doc
+
+class TherapySession(Document):
+ def on_submit(self):
+ self.update_sessions_count_in_therapy_plan()
+
+ def on_cancel(self):
+ self.update_sessions_count_in_therapy_plan(on_cancel=True)
+
+ def update_sessions_count_in_therapy_plan(self, on_cancel=False):
+ therapy_plan = frappe.get_doc('Therapy Plan', self.therapy_plan)
+ for entry in therapy_plan.therapy_plan_details:
+ if entry.therapy_type == self.therapy_type:
+ if on_cancel:
+ entry.sessions_completed -= 1
+ else:
+ entry.sessions_completed += 1
+ therapy_plan.save()
+
+
+@frappe.whitelist()
+def create_therapy_session(source_name, target_doc=None):
+ def set_missing_values(source, target):
+ therapy_type = frappe.get_doc('Therapy Type', source.therapy_type)
+ target.exercises = therapy_type.exercises
+
+ doc = get_mapped_doc('Patient Appointment', source_name, {
+ 'Patient Appointment': {
+ 'doctype': 'Therapy Session',
+ 'field_map': [
+ ['appointment', 'name'],
+ ['patient', 'patient'],
+ ['patient_age', 'patient_age'],
+ ['gender', 'patient_sex'],
+ ['therapy_type', 'therapy_type'],
+ ['therapy_plan', 'therapy_plan'],
+ ['practitioner', 'practitioner'],
+ ['department', 'department'],
+ ['start_date', 'appointment_date'],
+ ['start_time', 'appointment_time'],
+ ['service_unit', 'service_unit'],
+ ['company', 'company'],
+ ['invoiced', 'invoiced']
+ ]
+ }
+ }, target_doc, set_missing_values)
+
+ return doc
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py b/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py
new file mode 100644
index 00000000000..9de7e293238
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'therapy_session',
+ 'transactions': [
+ {
+ 'label': _('Assessments'),
+ 'items': ['Patient Assessment']
+ }
+ ]
+ }
diff --git a/erpnext/healthcare/doctype/therapy_type/__init__.py b/erpnext/healthcare/doctype/therapy_type/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py
new file mode 100644
index 00000000000..03a1be8a4e7
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py
@@ -0,0 +1,50 @@
+# -*- 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 TestTherapyType(unittest.TestCase):
+ def test_therapy_type_item(self):
+ therapy_type = create_therapy_type()
+ self.assertTrue(frappe.db.exists('Item', therapy_type.item))
+
+ therapy_type.disabled = 1
+ therapy_type.save()
+ self.assertEquals(frappe.db.get_value('Item', therapy_type.item, 'disabled'), 1)
+
+def create_therapy_type():
+ exercise = create_exercise_type()
+ therapy_type = frappe.db.exists('Therapy Type', 'Basic Rehab')
+ if not therapy_type:
+ therapy_type = frappe.new_doc('Therapy Type')
+ therapy_type.therapy_type = 'Basic Rehab'
+ therapy_type.default_duration = 30
+ therapy_type.is_billable = 1
+ therapy_type.rate = 5000
+ therapy_type.item_code = 'Basic Rehab'
+ therapy_type.item_name = 'Basic Rehab'
+ therapy_type.item_group = 'Services'
+ therapy_type.append('exercises', {
+ 'exercise_type': exercise.name,
+ 'counts_target': 10,
+ 'assistance_level': 'Passive'
+ })
+ therapy_type.save()
+ else:
+ therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab')
+ return therapy_type
+
+def create_exercise_type():
+ exercise_type = frappe.db.exists('Exercise Type', 'Sit to Stand')
+ if not exercise_type:
+ exercise_type = frappe.new_doc('Exercise Type')
+ exercise_type.exercise_name = 'Sit to Stand'
+ exercise_type.append('steps_table', {
+ 'title': 'Step 1',
+ 'description': 'Squat and Rise'
+ })
+ exercise_type.save()
+ return exercise_type
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.js b/erpnext/healthcare/doctype/therapy_type/therapy_type.js
new file mode 100644
index 00000000000..7a61b0def03
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.js
@@ -0,0 +1,93 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Therapy Type', {
+ setup: function(frm) {
+ frm.get_field('exercises').grid.editable_fields = [
+ {fieldname: 'exercise_type', columns: 7},
+ {fieldname: 'difficulty_level', columns: 1},
+ {fieldname: 'counts_target', columns: 1},
+ {fieldname: 'assistance_level', columns: 1}
+ ];
+ },
+
+ refresh: function(frm) {
+ if (!frm.doc.__islocal) {
+ cur_frm.add_custom_button(__('Change Item Code'), function() {
+ change_template_code(frm.doc);
+ });
+ }
+ },
+
+ therapy_type: function(frm) {
+ if (!frm.doc.item_code)
+ frm.set_value('item_code', frm.doc.therapy_type);
+ if (!frm.doc.description)
+ frm.set_value('description', frm.doc.therapy_type);
+ mark_change_in_item(frm);
+ },
+
+ rate: function(frm) {
+ mark_change_in_item(frm);
+ },
+
+ is_billable: function (frm) {
+ mark_change_in_item(frm);
+ },
+
+ item_group: function(frm) {
+ mark_change_in_item(frm);
+ },
+
+ description: function(frm) {
+ mark_change_in_item(frm);
+ },
+
+ medical_department: function(frm) {
+ mark_change_in_item(frm);
+ }
+});
+
+let mark_change_in_item = function(frm) {
+ if (!frm.doc.__islocal) {
+ frm.doc.change_in_item = 1;
+ }
+};
+
+let change_template_code = function(doc) {
+ let d = new frappe.ui.Dialog({
+ title:__('Change Item Code'),
+ fields:[
+ {
+ 'fieldtype': 'Data',
+ 'label': 'Item Code',
+ 'fieldname': 'item_code',
+ reqd: 1
+ }
+ ],
+ primary_action: function() {
+ let values = d.get_values();
+
+ if (values) {
+ frappe.call({
+ 'method': 'erpnext.healthcare.doctype.therapy_type.therapy_type.change_item_code_from_therapy',
+ 'args': {item_code: values.item_code, doc: doc},
+ callback: function () {
+ cur_frm.reload_doc();
+ frappe.show_alert({
+ message: 'Item Code renamed successfully',
+ indicator: 'green'
+ });
+ }
+ });
+ }
+ d.hide();
+ },
+ primary_action_label: __('Change Item Code')
+ });
+ d.show();
+
+ d.set_values({
+ 'item_code': doc.item_code
+ });
+};
diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.json b/erpnext/healthcare/doctype/therapy_type/therapy_type.json
new file mode 100644
index 00000000000..0b3c3caeaab
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.json
@@ -0,0 +1,211 @@
+{
+ "actions": [],
+ "autoname": "field:therapy_type",
+ "creation": "2020-03-29 20:48:31.715063",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "disabled",
+ "section_break_2",
+ "therapy_type",
+ "default_duration",
+ "medical_department",
+ "column_break_3",
+ "is_billable",
+ "rate",
+ "healthcare_service_unit",
+ "item_details_section",
+ "item",
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_12",
+ "description",
+ "section_break_18",
+ "therapy_for",
+ "add_exercises",
+ "section_break_6",
+ "exercises",
+ "change_in_item"
+ ],
+ "fields": [
+ {
+ "fieldname": "therapy_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Therapy Type",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_billable",
+ "fieldtype": "Check",
+ "label": "Is Billable"
+ },
+ {
+ "depends_on": "eval:doc.is_billable;",
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "label": "Rate",
+ "mandatory_depends_on": "eval:doc.is_billable;"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break",
+ "label": "Exercises"
+ },
+ {
+ "fieldname": "exercises",
+ "fieldtype": "Table",
+ "label": "Exercises",
+ "options": "Exercise"
+ },
+ {
+ "fieldname": "default_duration",
+ "fieldtype": "Int",
+ "label": "Default Duration (In Minutes)"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "item_details_section",
+ "fieldtype": "Section Break",
+ "label": "Item Details"
+ },
+ {
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Data",
+ "label": "Item Code",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "medical_department",
+ "fieldtype": "Link",
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "default": "0",
+ "fieldname": "change_in_item",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Change In Item",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "therapy_for",
+ "fieldtype": "Table MultiSelect",
+ "label": "Therapy For",
+ "options": "Body Part Link"
+ },
+ {
+ "fieldname": "healthcare_service_unit",
+ "fieldtype": "Link",
+ "label": "Healthcare Service Unit",
+ "options": "Healthcare Service Unit"
+ },
+ {
+ "depends_on": "eval: doc.therapy_for",
+ "fieldname": "add_exercises",
+ "fieldtype": "Button",
+ "label": "Add Exercises",
+ "options": "add_exercises"
+ },
+ {
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break"
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-21 13:09:04.006289",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Therapy Type",
+ "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": "Healthcare Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Physician",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.py b/erpnext/healthcare/doctype/therapy_type/therapy_type.py
new file mode 100644
index 00000000000..ea3d84e7c5d
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.py
@@ -0,0 +1,122 @@
+# -*- 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
+import json
+from frappe import _
+from frappe.utils import cint
+from frappe.model.document import Document
+from frappe.model.rename_doc import rename_doc
+
+class TherapyType(Document):
+ def validate(self):
+ self.enable_disable_item()
+
+ def after_insert(self):
+ create_item_from_therapy(self)
+
+ def on_update(self):
+ if self.change_in_item:
+ self.update_item_and_item_price()
+
+ def enable_disable_item(self):
+ if self.is_billable:
+ if self.disabled:
+ frappe.db.set_value('Item', self.item, 'disabled', 1)
+ else:
+ frappe.db.set_value('Item', self.item, 'disabled', 0)
+
+ def update_item_and_item_price(self):
+ if self.is_billable and self.item:
+ item_doc = frappe.get_doc('Item', {'item_code': self.item})
+ item_doc.item_name = self.item_name
+ item_doc.item_group = self.item_group
+ item_doc.description = self.description
+ item_doc.disabled = 0
+ item_doc.ignore_mandatory = True
+ item_doc.save(ignore_permissions=True)
+
+ if self.rate:
+ item_price = frappe.get_doc('Item Price', {'item_code': self.item})
+ item_price.item_name = self.item_name
+ item_price.price_list_name = self.rate
+ item_price.ignore_mandatory = True
+ item_price.save()
+
+ elif not self.is_billable and self.item:
+ frappe.db.set_value('Item', self.item, 'disabled', 1)
+
+ self.db_set('change_in_item', 0)
+
+ def add_exercises(self):
+ exercises = self.get_exercises_for_body_parts()
+ last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,])
+ for i, d in enumerate(exercises):
+ ch = self.append('exercises', {})
+ ch.exercise_type = d.parent
+ ch.idx = last_idx + i + 1
+
+ def get_exercises_for_body_parts(self):
+ body_parts = [entry.body_part for entry in self.therapy_for]
+
+ exercises = frappe.db.sql(
+ """
+ SELECT DISTINCT
+ b.parent, e.name, e.difficulty_level
+ FROM
+ `tabExercise Type` e, `tabBody Part Link` b
+ WHERE
+ b.body_part IN %(body_parts)s AND b.parent=e.name
+ """, {'body_parts': body_parts}, as_dict=1)
+
+ return exercises
+
+
+def create_item_from_therapy(doc):
+ disabled = doc.disabled
+ if doc.is_billable and not doc.disabled:
+ disabled = 0
+
+ uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom')
+
+ item = frappe.get_doc({
+ 'doctype': 'Item',
+ 'item_code': doc.item_code,
+ 'item_name': doc.item_name,
+ 'item_group': doc.item_group,
+ 'description': doc.description,
+ 'is_sales_item': 1,
+ 'is_service_item': 1,
+ 'is_purchase_item': 0,
+ 'is_stock_item': 0,
+ 'show_in_website': 0,
+ 'is_pro_applicable': 0,
+ 'disabled': disabled,
+ 'stock_uom': uom
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ make_item_price(item.name, doc.rate)
+ doc.db_set('item', item.name)
+
+
+def make_item_price(item, item_price):
+ price_list_name = frappe.db.get_value('Price List', {'selling': 1})
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'price_list': price_list_name,
+ 'item_code': item,
+ 'price_list_rate': item_price
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
+
+@frappe.whitelist()
+def change_item_code_from_therapy(item_code, doc):
+ doc = frappe._dict(json.loads(doc))
+
+ if frappe.db.exists('Item', {'item_code': item_code}):
+ frappe.throw(_('Item with Item Code {0} already exists').format(item_code))
+ else:
+ rename_doc('Item', doc.item, item_code, ignore_permissions=True)
+ frappe.db.set_value('Therapy Type', doc.name, 'item_code', item_code)
+ return
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index 17d8f642928..00a50b56525 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -23,6 +23,8 @@ def get_healthcare_services_to_invoice(patient, company):
items_to_invoice += get_lab_tests_to_invoice(patient, company)
items_to_invoice += get_clinical_procedures_to_invoice(patient, company)
items_to_invoice += get_inpatient_services_to_invoice(patient, company)
+ items_to_invoice += get_therapy_sessions_to_invoice(patient, company)
+
return items_to_invoice
@@ -245,6 +247,25 @@ def get_inpatient_services_to_invoice(patient, company):
return services_to_invoice
+def get_therapy_sessions_to_invoice(patient, company):
+ therapy_sessions_to_invoice = []
+ therapy_sessions = frappe.get_list(
+ 'Therapy Session',
+ fields='*',
+ filters={'patient': patient.name, 'invoiced': 0, 'company': company}
+ )
+ for therapy in therapy_sessions:
+ if not therapy.appointment:
+ if therapy.therapy_type and frappe.db.get_value('Therapy Type', therapy.therapy_type, 'is_billable'):
+ therapy_sessions_to_invoice.append({
+ 'reference_type': 'Therapy Session',
+ 'reference_name': therapy.name,
+ 'service': frappe.db.get_value('Therapy Type', therapy.therapy_type, 'item')
+ })
+
+ return therapy_sessions_to_invoice
+
+
def get_service_item_and_practitioner_charge(doc):
is_inpatient = doc.inpatient_record
if is_inpatient:
diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py
index b3c803b5644..223c4e3e3bf 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.py
@@ -776,22 +776,16 @@ class SalarySlip(TransactionBase):
for payment in self.get('loans'):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
+ total_amount = amounts['interest_amount'] + amounts['payable_principal_amount']
+ if payment.total_payment > total_amount:
+ frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2}
+ against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment),
+ frappe.bold(total_amount), frappe.bold(payment.loan)))
- if payment.interest_amount > amounts['interest_amount']:
- frappe.throw(_("""Row {0}: Paid Interest amount {1} is greater than pending interest amount {2}
- against loan {3}""").format(payment.idx, frappe.bold(payment.interest_amount),
- frappe.bold(amounts['interest_amount']), frappe.bold(payment.loan)))
-
- if payment.principal_amount > amounts['payable_principal_amount']:
- frappe.throw(_("""Row {0}: Paid Principal amount {1} is greater than pending principal amount {2}
- against loan {3}""").format(payment.idx, frappe.bold(payment.principal_amount),
- frappe.bold(amounts['payable_principal_amount']), frappe.bold(payment.loan)))
-
- payment.total_payment = payment.interest_amount + payment.principal_amount
self.total_interest_amount += payment.interest_amount
self.total_principal_amount += payment.principal_amount
- self.total_loan_repayment = self.total_interest_amount + self.total_principal_amount
+ self.total_loan_repayment += payment.total_payment
def get_loan_details(self):
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 108672b25ae..2d1ad33ed01 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -149,13 +149,19 @@ class TestLoan(unittest.TestCase):
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
repayment_entry.save()
+ repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
-
- self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
- repayment_entry.submit()
+ amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
+ 'paid_principal_amount'])
+
+ loan.load_from_db()
+
+ self.assertEquals(amounts[0], repayment_entry.interest_payable)
+ self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid -
+ penalty_amount - amounts[0], 2))
def test_loan_closure_repayment(self):
pledges = []
@@ -189,15 +195,19 @@ class TestLoan(unittest.TestCase):
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
- "Loan Closure", 13315.0681)
- repayment_entry.save()
+ "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry.submit()
- repayment_entry.amount_paid = repayment_entry.payable_amount
+ amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
+ 'paid_principal_amount'])
- self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3))
+ unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \
+ / (days_in_year(get_datetime(first_date).year) * 100)
+
+ self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3),
+ flt(accrued_interest_amount, 3))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
- repayment_entry.submit()
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
@@ -227,57 +237,15 @@ class TestLoan(unittest.TestCase):
process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5),
- "Regular Payment", 89768.7534247)
+ "Regular Payment", 89768.75)
- repayment_entry.save()
repayment_entry.submit()
- repayment_entry.load_from_db()
+ amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
+ 'paid_principal_amount'])
- self.assertEquals(repayment_entry.interest_payable, 11250.00)
- self.assertEquals(repayment_entry.payable_principal_amount, 78303.00)
-
- def test_partial_loan_repayment(self):
- pledges = []
- pledges.append({
- "loan_security": "Test Security 1",
- "qty": 4000.00,
- "haircut": 50
- })
-
- loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
-
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
- posting_date=get_first_day(nowdate()))
-
- loan.submit()
-
- self.assertEquals(loan.loan_amount, 1000000)
-
- first_date = '2019-10-01'
- last_date = '2019-10-30'
-
- no_of_days = date_diff(last_date, first_date) + 1
-
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime().year) * 100)
-
- make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
-
- process_loan_interest_accrual_for_demand_loans(posting_date = add_days(first_date, 15))
- process_loan_interest_accrual_for_demand_loans(posting_date = add_days(first_date, 30))
-
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500)
- repayment_entry.save()
- repayment_entry.submit()
-
- penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
-
- lia1 = frappe.get_value("Loan Interest Accrual", {"loan": loan.name, "is_paid": 1}, 'name')
- lia2 = frappe.get_value("Loan Interest Accrual", {"loan": loan.name, "is_paid": 0}, 'name')
-
- self.assertTrue(lia1)
- self.assertTrue(lia2)
+ self.assertEquals(amounts[0], 11250.00)
+ self.assertEquals(amounts[1], 78303.00)
def test_security_shortfall(self):
pledges = []
@@ -294,7 +262,7 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount)
- frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 100
+ frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
where loan_security='Test Security 2'""")
create_process_loan_security_shortfall()
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
index a26112011ca..5fc3e8f4b60 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
@@ -15,12 +15,13 @@
"company",
"posting_date",
"is_term_loan",
- "is_paid",
"section_break_7",
"pending_principal_amount",
"payable_principal_amount",
+ "paid_principal_amount",
"column_break_14",
"interest_amount",
+ "paid_interest_amount",
"section_break_15",
"process_loan_interest_accrual",
"repayment_schedule_name",
@@ -101,13 +102,6 @@
"label": "Company",
"options": "Company"
},
- {
- "default": "0",
- "fieldname": "is_paid",
- "fieldtype": "Check",
- "label": "Is Paid",
- "read_only": 1
- },
{
"default": "0",
"fetch_from": "loan.is_term_loan",
@@ -143,12 +137,24 @@
"hidden": 1,
"label": "Repayment Schedule Name",
"read_only": 1
+ },
+ {
+ "fieldname": "paid_principal_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Principal Amount",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "paid_interest_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Interest Amount",
+ "options": "Company:company:default_currency"
}
],
"in_create": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-10 18:31:02.369857",
+ "modified": "2020-04-16 11:24:23.258404",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 4b930c50aef..789c1299463 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -30,9 +30,8 @@
"reference_number",
"column_break_21",
"reference_date",
- "paid_accrual_entries",
- "partial_paid_entry",
"principal_amount_paid",
+ "repayment_details",
"amended_from"
],
"fields": [
@@ -155,13 +154,6 @@
"options": "Company:company:default_currency",
"read_only": 1
},
- {
- "fieldname": "paid_accrual_entries",
- "fieldtype": "Text",
- "hidden": 1,
- "label": "Paid Accrual Entries",
- "read_only": 1
- },
{
"default": "0",
"fetch_from": "against_loan.is_term_loan",
@@ -197,13 +189,6 @@
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
- {
- "fieldname": "partial_paid_entry",
- "fieldtype": "Text",
- "hidden": 1,
- "label": "Partial Paid Entry",
- "read_only": 1
- },
{
"default": "0.0",
"fieldname": "principal_amount_paid",
@@ -225,11 +210,18 @@
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
+ },
+ {
+ "fieldname": "repayment_details",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Repayment Details",
+ "options": "Loan Repayment Detail"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-02-26 06:18:54.934538",
+ "modified": "2020-04-16 18:14:45.166754",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
@@ -264,7 +256,6 @@
"write": 1
}
],
- "quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 2d2ca4c2f4e..87e8a15ab48 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -19,11 +19,11 @@ class LoanRepayment(AccountsController):
def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
self.set_missing_values(amounts)
-
- def before_submit(self):
- self.mark_as_paid()
+ self.validate_amount()
+ self.allocate_amounts(amounts['pending_accrual_entries'])
def on_submit(self):
+ self.update_paid_amount()
self.make_gl_entries()
def on_cancel(self):
@@ -38,32 +38,25 @@ class LoanRepayment(AccountsController):
self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.interest_payable:
- self.interest_payable = amounts['interest_amount']
+ self.interest_payable = flt(amounts['interest_amount'], 2)
if not self.penalty_amount:
- self.penalty_amount = amounts['penalty_amount']
+ self.penalty_amount = flt(amounts['penalty_amount'], 2)
if not self.pending_principal_amount:
- self.pending_principal_amount = amounts['pending_principal_amount']
+ self.pending_principal_amount = flt(amounts['pending_principal_amount'], 2)
if not self.payable_principal_amount and self.is_term_loan:
- self.payable_principal_amount = amounts['payable_principal_amount']
+ self.payable_principal_amount = flt(amounts['payable_principal_amount'], 2)
if not self.payable_amount:
- self.payable_amount = amounts['payable_amount']
-
- if amounts.get('paid_accrual_entries'):
- self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries'))
+ self.payable_amount = flt(amounts['payable_amount'], 2)
if amounts.get('due_date'):
self.due_date = amounts.get('due_date')
- def mark_as_paid(self):
- paid_entries = []
- paid_amount = self.amount_paid
- interest_paid = paid_amount
-
- if not paid_amount:
+ def validate_amount(self):
+ if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero"))
if self.amount_paid < self.penalty_amount:
@@ -74,37 +67,15 @@ class LoanRepayment(AccountsController):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg)
+ def update_paid_amount(self):
loan = frappe.get_doc("Loan", self.against_loan)
- if self.paid_accrual_entries:
- paid_accrual_entries = json.loads(self.paid_accrual_entries)
-
- if paid_amount - self.penalty_amount > 0 and self.paid_accrual_entries:
-
- interest_paid = paid_amount - self.penalty_amount
-
- for lia, interest_amount in iteritems(paid_accrual_entries):
- if interest_amount <= interest_paid:
- paid_entries.append(lia)
- interest_paid -= interest_amount
- elif interest_paid:
- self.partial_paid_entry = frappe.as_json({"name": lia, "interest_amount": interest_amount})
- frappe.db.set_value("Loan Interest Accrual", lia, "interest_amount",
- interest_amount - interest_paid)
- interest_paid = 0
-
- if paid_entries:
- self.paid_accrual_entries = frappe.as_json(paid_entries)
- else:
- self.paid_accrual_entries = ""
-
- if interest_paid:
- self.principal_amount_paid = interest_paid
-
- if paid_entries:
- frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
- SET is_paid = 1 where name in (%s)""" #nosec
- % ", ".join(['%s']*len(paid_entries)), tuple(paid_entries))
+ for payment in self.repayment_details:
+ frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
+ SET paid_principal_amount = `paid_principal_amount` + %s,
+ paid_interest_amount = `paid_interest_amount` + %s
+ WHERE name = %s""",
+ (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2):
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
@@ -116,21 +87,14 @@ class LoanRepayment(AccountsController):
update_shortfall_status(self.against_loan, self.principal_amount_paid)
def mark_as_unpaid(self):
-
loan = frappe.get_doc("Loan", self.against_loan)
- if self.paid_accrual_entries:
- paid_accrual_entries = json.loads(self.paid_accrual_entries)
-
- if self.paid_accrual_entries:
- frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
- SET is_paid = 0 where name in (%s)""" #nosec
- % ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries))
-
- if self.partial_paid_entry:
- partial_paid_entry = json.loads(self.partial_paid_entry)
- frappe.db.set_value("Loan Interest Accrual", partial_paid_entry["name"], "interest_amount",
- partial_paid_entry["interest_amount"])
+ for payment in self.repayment_details:
+ frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
+ SET paid_principal_amount = `paid_principal_amount` - %s,
+ paid_interest_amount = `paid_interest_amount` - %s
+ WHERE name = %s""",
+ (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
@@ -139,6 +103,38 @@ class LoanRepayment(AccountsController):
if loan.status == "Loan Closure Requested":
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
+ def allocate_amounts(self, paid_entries):
+ self.set('repayment_details', [])
+ self.principal_amount_paid = 0
+
+ if self.amount_paid - self.penalty_amount > 0 and paid_entries:
+ interest_paid = self.amount_paid - self.penalty_amount
+ for lia, amounts in iteritems(paid_entries):
+ if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
+ interest_amount = amounts['interest_amount']
+ paid_principal = amounts['payable_principal_amount']
+ self.principal_amount_paid += paid_principal
+ interest_paid -= (interest_amount + paid_principal)
+ elif interest_paid:
+ if interest_paid >= amounts['interest_amount']:
+ interest_amount = amounts['interest_amount']
+ paid_principal = interest_paid - interest_amount
+ self.principal_amount_paid += paid_principal
+ interest_paid = 0
+ else:
+ interest_amount = interest_paid
+ interest_paid = 0
+ paid_principal=0
+
+ self.append('repayment_details', {
+ 'loan_interest_accrual': lia,
+ 'paid_interest_amount': interest_amount,
+ 'paid_principal_amount': paid_principal
+ })
+
+ if interest_paid:
+ self.principal_amount_paid += interest_paid
+
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
@@ -223,7 +219,7 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
"posting_date": posting_date,
"applicant": applicant,
"penalty_amount": penalty_amount,
- "interst_payable": interest_payable,
+ "interest_payable": interest_payable,
"payable_principal_amount": payable_principal_amount,
"amount_paid": amount_paid,
"loan_type": loan_type
@@ -232,15 +228,22 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
return lr
def get_accrued_interest_entries(against_loan):
- accrued_interest_entries = frappe.get_all("Loan Interest Accrual",
- fields=["name", "interest_amount", "posting_date", "payable_principal_amount"],
- filters = {
- "loan": against_loan,
- "is_paid": 0,
- "docstatus": 1
- }, order_by="posting_date")
- return accrued_interest_entries
+ unpaid_accrued_entries = frappe.db.sql(
+ """
+ SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
+ payable_principal_amount - paid_principal_amount as payable_principal_amount
+ FROM
+ `tabLoan Interest Accrual`
+ WHERE
+ loan = %s
+ AND (interest_amount - paid_interest_amount > 0 OR
+ payable_principal_amount - paid_principal_amount > 0)
+ AND
+ docstatus = 1
+ """, (against_loan), as_dict=1)
+
+ return unpaid_accrued_entries
# This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
@@ -273,8 +276,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount
- pending_accrual_entries.setdefault(entry.name,
- flt(entry.interest_amount) + flt(entry.payable_principal_amount))
+ pending_accrual_entries.setdefault(entry.name, {
+ 'interest_amount': flt(entry.interest_amount),
+ 'payable_principal_amount': flt(entry.payable_principal_amount)
+ })
final_due_date = due_date
@@ -291,7 +296,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
amounts["interest_amount"] = total_pending_interest
amounts["penalty_amount"] = penalty_amount
amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount
- amounts["paid_accrual_entries"] = pending_accrual_entries
+ amounts["pending_accrual_entries"] = pending_accrual_entries
if final_due_date:
amounts["due_date"] = final_due_date
diff --git a/erpnext/loan_management/doctype/loan_repayment_detail/__init__.py b/erpnext/loan_management/doctype/loan_repayment_detail/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json
new file mode 100644
index 00000000000..cff1dbb1d29
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "creation": "2020-04-15 18:31:54.026923",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "loan_interest_accrual",
+ "paid_principal_amount",
+ "paid_interest_amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "loan_interest_accrual",
+ "fieldtype": "Link",
+ "label": "Loan Interest Accrual",
+ "options": "Loan Interest Accrual"
+ },
+ {
+ "fieldname": "paid_principal_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Principal Amount",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "paid_interest_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Interest Amount",
+ "options": "Company:company:default_currency"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-04-15 21:50:03.837019",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Repayment Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.py b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.py
new file mode 100644
index 00000000000..a83b9b59415
--- /dev/null
+++ b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.py
@@ -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 LoanRepaymentDetail(Document):
+ pass
diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
index f7e211656e7..2f4fe249456 100644
--- a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
+++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
@@ -28,7 +28,6 @@
{
"fieldname": "loan_account",
"fieldtype": "Link",
- "in_list_view": 1,
"label": "Loan Account",
"options": "Account",
"read_only": 1
@@ -50,21 +49,23 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Principal Amount",
- "options": "Company:company:default_currency"
+ "options": "Company:company:default_currency",
+ "read_only": 1
},
{
"fieldname": "interest_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Interest Amount",
- "options": "Company:company:default_currency"
+ "options": "Company:company:default_currency",
+ "read_only": 1
},
{
"fieldname": "total_payment",
"fieldtype": "Currency",
+ "in_list_view": 1,
"label": "Total Payment",
- "options": "Company:company:default_currency",
- "read_only": 1
+ "options": "Company:company:default_currency"
},
{
"fieldname": "loan_repayment_entry",
@@ -84,7 +85,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-04-09 20:01:53.546364",
+ "modified": "2020-04-16 13:17:04.798335",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Salary Slip Loan",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 8c7876d48de..bab0dfb6b43 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -82,7 +82,9 @@ frappe.ui.form.on('Job Card', {
frm.set_value('current_time' , 0);
}
- frm.save();
+ frm.save("Save", () => {}, "", () => {
+ frm.doc.time_logs.pop(-1);
+ });
},
complete_job: function(frm, completed_time, completed_qty) {
@@ -105,6 +107,24 @@ frappe.ui.form.on('Job Card', {
});
},
+ validate: function(frm) {
+ if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
+ frm.trigger("reset_timer");
+ }
+ },
+
+ employee: function(frm) {
+ if (frm.doc.job_started && !frm.doc.current_time) {
+ frm.trigger("reset_timer");
+ }
+ },
+
+ reset_timer: function(frm) {
+ frm.set_value('started_time' , '');
+ frm.set_value('job_started', 0);
+ frm.set_value('current_time' , 0);
+ },
+
make_dashboard: function(frm) {
if(frm.doc.__islocal)
return;
@@ -137,12 +157,12 @@ frappe.ui.form.on('Job Card', {
updateStopwatch(current);
}, 1000);
}
-
+
function updateStopwatch(increment) {
var hours = Math.floor(increment / 3600);
var minutes = Math.floor((increment - (hours * 3600)) / 60);
var seconds = increment - (hours * 3600) - (minutes * 60);
-
+
$(section).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
$(section).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
$(section).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
@@ -205,5 +225,10 @@ frappe.ui.form.on('Job Card', {
frappe.ui.form.on('Job Card Time Log', {
completed_qty: function(frm) {
frm.events.set_total_completed_qty(frm);
+ },
+
+ to_time: function(frm) {
+ frm.set_value('job_started', 0);
+ frm.set_value('started_time', '');
}
})
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index f8c60f2a114..e9627a55145 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
- get_time, add_to_date, time_diff, add_days, get_datetime_str)
+ get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@@ -189,11 +189,15 @@ class JobCard(Document):
def validate_job_card(self):
if not self.time_logs:
- frappe.throw(_("Time logs are required for job card {0}").format(self.name))
+ frappe.throw(_("Time logs are required for {0} {1}")
+ .format(frappe.bold("Job Card"), get_link_to_form("Job Card", self.name)))
if self.for_quantity and self.total_completed_qty != self.for_quantity:
- frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})"
- .format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity))))
+ total_completed_qty = frappe.bold(_("Total Completed Qty"))
+ qty_to_manufacture = frappe.bold(_("Qty to Manufacture"))
+
+ frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})"
+ .format(total_completed_qty, frappe.bold(self.total_completed_qty), qty_to_manufacture,frappe.bold(self.for_quantity))))
def update_work_order(self):
if not self.work_order:
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 96e5cd57c35..b49b0ba0f74 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -188,7 +188,7 @@ frappe.ui.form.on('Production Plan', {
},
get_items_for_mr: function(frm) {
- const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom',
+ const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom',
'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type'];
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 9c8aa453a68..d541866f8b7 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -240,6 +240,8 @@ frappe.ui.form.on("Work Order", {
});
}, __("Job Card"), __("Create"));
+ dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide();
+
var pending_qty = 0;
frm.doc.operations.forEach(data => {
if(data.completed_qty != frm.doc.qty) {
diff --git a/erpnext/non_profit/desk_page/non_profit/non_profit.json b/erpnext/non_profit/desk_page/non_profit/non_profit.json
index a476857f099..ebe61948935 100644
--- a/erpnext/non_profit/desk_page/non_profit/non_profit.json
+++ b/erpnext/non_profit/desk_page/non_profit/non_profit.json
@@ -42,7 +42,7 @@
"idx": 0,
"is_standard": 1,
"label": "Non Profit",
- "modified": "2020-04-01 11:28:51.430882",
+ "modified": "2020-04-13 13:41:52.373705",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
@@ -50,5 +50,31 @@
"pin_to_bottom": 0,
"pin_to_top": 0,
"restrict_to_domain": "Non Profit",
- "shortcuts": []
+ "shortcuts": [
+ {
+ "label": "Member",
+ "link_to": "Member",
+ "type": "DocType"
+ },
+ {
+ "label": "Membership Settings",
+ "link_to": "Membership Settings",
+ "type": "DocType"
+ },
+ {
+ "label": "Membership",
+ "link_to": "Membership",
+ "type": "DocType"
+ },
+ {
+ "label": "Chapter",
+ "link_to": "Chapter",
+ "type": "DocType"
+ },
+ {
+ "label": "Chapter Member",
+ "link_to": "Chapter Member",
+ "type": "DocType"
+ }
+ ]
}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js
index eb74dc1507e..3e9d0baba5f 100644
--- a/erpnext/non_profit/doctype/member/member.js
+++ b/erpnext/non_profit/doctype/member/member.js
@@ -2,6 +2,14 @@
// For license information, please see license.txt
frappe.ui.form.on('Member', {
+ setup: function(frm) {
+ frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
+ frm.set_df_property('razorpay_details_section', 'hidden', false);
+ }
+ })
+ },
+
refresh: function(frm) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Member'};
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index 941497494c8..bb73a843eeb 100644
--- a/erpnext/non_profit/doctype/member/member.json
+++ b/erpnext/non_profit/doctype/member/member.json
@@ -1,604 +1,216 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "autoname": "naming_series:",
- "beta": 0,
- "creation": "2017-09-11 09:24:52.898356",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "naming_series:",
+ "creation": "2017-09-11 09:24:52.898356",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "member_name",
+ "membership_expiry_date",
+ "column_break_5",
+ "membership_type",
+ "email",
+ "email_id",
+ "image",
+ "customer_section",
+ "customer",
+ "customer_name",
+ "supplier_section",
+ "supplier",
+ "address_contacts",
+ "address_html",
+ "column_break_9",
+ "contact_html",
+ "razorpay_details_section",
+ "subscription_id",
+ "customer_id",
+ "subscription_activated",
+ "column_break_21",
+ "subscription_start",
+ "subscription_end"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Series",
- "length": 0,
- "no_copy": 0,
- "options": "NPO-MEM-.YYYY.-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "options": "NPO-MEM-.YYYY.-",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "member_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Member Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "member_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Member Name",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "membership_expiry_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Membership Expiry Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "membership_expiry_date",
+ "fieldtype": "Date",
+ "label": "Membership Expiry Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_5",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "membership_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Membership Type",
- "length": 0,
- "no_copy": 0,
- "options": "Membership Type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "membership_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Membership Type",
+ "options": "Membership Type",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Email",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "email",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Image",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "image",
+ "fieldtype": "Attach Image",
+ "hidden": 1,
+ "label": "Image",
+ "no_copy": 1,
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "customer_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "collapsible": 1,
+ "fieldname": "customer_section",
+ "fieldtype": "Section Break",
+ "label": "Customer"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "options": "Customer",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "label": "Customer",
+ "options": "Customer"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "customer.customer_name",
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Customer Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "customer.customer_name",
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "label": "Customer Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "supplier_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Supplier",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "collapsible": 1,
+ "fieldname": "supplier_section",
+ "fieldtype": "Section Break",
+ "label": "Supplier"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "supplier",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Supplier",
- "length": 0,
- "no_copy": 0,
- "options": "Supplier",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "label": "Supplier",
+ "options": "Supplier"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address_contacts",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Address and Contact",
- "length": 0,
- "no_copy": 0,
- "options": "fa fa-map-marker",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "address_contacts",
+ "fieldtype": "Section Break",
+ "label": "Address and Contact",
+ "options": "fa fa-map-marker"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Address HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "address_html",
+ "fieldtype": "HTML",
+ "label": "Address HTML"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Contact HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "contact_html",
+ "fieldtype": "HTML",
+ "label": "Contact HTML"
+ },
+ {
+ "fieldname": "email_id",
+ "fieldtype": "Data",
+ "label": "Email Address"
+ },
+ {
+ "fieldname": "subscription_id",
+ "fieldtype": "Data",
+ "label": "Subscription ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "customer_id",
+ "fieldtype": "Data",
+ "label": "Customer ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "razorpay_details_section",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Razorpay Details"
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "subscription_activated",
+ "fieldtype": "Check",
+ "label": "Subscription Activated"
+ },
+ {
+ "fieldname": "subscription_start",
+ "fieldtype": "Date",
+ "label": "Subscription Start "
+ },
+ {
+ "fieldname": "subscription_end",
+ "fieldtype": "Date",
+ "label": "Subscription End"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_field": "image",
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 14:44:23.218109",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Member",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "image_field": "image",
+ "links": [],
+ "modified": "2020-04-07 14:20:33.215700",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Member",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Member",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Member",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "member_name",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "quick_entry": 1,
+ "restrict_to_domain": "Non Profit",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "member_name",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 9afaf90e7ab..571f87af874 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -3,9 +3,12 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact
-
+from frappe.utils import cint
+from frappe.integrations.utils import get_payment_gateway_controller
class Member(Document):
def onload(self):
@@ -14,8 +17,114 @@ class Member(Document):
def validate(self):
- self.validate_email_type(self.email)
+ if self.email:
+ self.validate_email_type(self.email)
+ if self.email_id:
+ self.validate_email_type(self.email_id)
def validate_email_type(self, email):
from frappe.utils import validate_email_address
- validate_email_address(email.strip(), True)
\ No newline at end of file
+ validate_email_address(email.strip(), True)
+
+ def setup_subscription(self):
+ membership_settings = frappe.get_doc("Membership Settings")
+ if not membership_settings.enable_razorpay:
+ frappe.throw("Please enable Razorpay to setup subscription")
+
+ controller = get_payment_gateway_controller("Razorpay")
+ settings = controller.get_settings({})
+
+ plan_id = frappe.get_value("Membership Type", self.membership_type, "razorpay_plan_id")
+
+ if not plan_id:
+ frappe.throw(_("Please setup Razorpay Plan ID"))
+
+ subscription_details = {
+ "plan_id": plan_id,
+ "billing_frequency": cint(membership_settings.billing_frequency),
+ "customer_notify": 1
+ }
+
+ args = {
+ 'subscription_details': subscription_details
+ }
+
+ subscription = controller.setup_subscription(settings, **args)
+
+ return subscription
+
+def get_or_create_member(user_details):
+ member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id})
+ if member_list and member_list[0]:
+ return member_list[0]['name']
+ else:
+ return create_member(user_details)
+
+def create_member(user_details):
+ member = frappe.new_doc("Member")
+ member.update({
+ "member_name": user_details.fullname,
+ "email_id": user_details.email,
+ "pan_number": user_details.pan,
+ "membership_type": user_details.plan_id,
+ "customer": create_customer(user_details)
+ })
+
+ member.insert(ignore_permissions=True)
+ return member
+
+def create_customer(user_details):
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = user_details.fullname
+ customer.customer_type = "Individual"
+ customer.insert(ignore_permissions=True)
+
+ try:
+ contact = frappe.new_doc("Contact")
+ contact.first_name = user_details.fullname
+ contact.add_phone(user_details.mobile, is_primary_phone=1, is_primary_mobile_no=1)
+ contact.add_email(user_details.email, is_primary=1)
+ contact.insert(ignore_permissions=True)
+
+ contact.append("links", {
+ "link_doctype": "Customer",
+ "link_name": customer.name
+ })
+
+ contact.insert()
+ except Exception as e:
+ frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
+ pass
+
+ return customer.name
+
+@frappe.whitelist(allow_guest=True)
+def create_member_subscription_order(user_details):
+ """Create Member subscription and order for payment
+
+ Args:
+ user_details (TYPE): Description
+
+ Returns:
+ Dictionary: Dictionary with subscription details
+ {
+ 'subscription_details': {
+ 'plan_id': 'plan_EXwyxDYDCj3X4v',
+ 'billing_frequency': 24,
+ 'customer_notify': 1
+ },
+ 'subscription_id': 'sub_EZycCvXFvqnC6p'
+ }
+ """
+ # {"plan_id":"IFF Starter","fullname":"Shivam Mishra","mobile":"7506056962","email":"shivam@shivam.dev","pan":"Testing123"}
+ user_details = frappe._dict(user_details)
+ member = get_or_create_member(user_details)
+ if not member:
+ member = create_member(user_details)
+
+ subscription = member.setup_subscription()
+
+ member.subscription_id = subscription.get('subscription_id')
+ member.save(ignore_permissions=True)
+
+ return subscription
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index 421087995a5..554549a0bd1 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -2,7 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Membership', {
- onload:function(frm) {
+ setup: function(frm) {
+ frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ if (val) frm.set_df_property('razorpay_details_section', 'hidden', false);
+ })
+ },
+
+ onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount');
}
});
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 9a204b19bfc..9f10d0cfc70 100644
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ b/erpnext/non_profit/doctype/membership/membership.json
@@ -1,501 +1,164 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "NPO-MSH-.YYYY.-.#####",
- "beta": 0,
- "creation": "2017-09-11 11:39:18.492184",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "NPO-MSH-.YYYY.-.#####",
+ "creation": "2017-09-11 11:39:18.492184",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "member",
+ "membership_type",
+ "column_break_3",
+ "membership_status",
+ "membership_validity_section",
+ "from_date",
+ "to_date",
+ "column_break_8",
+ "member_since_date",
+ "payment_details",
+ "paid",
+ "currency",
+ "amount",
+ "razorpay_details_section",
+ "subscription_id",
+ "payment_id",
+ "webhook_payload"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "member",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Member",
- "length": 0,
- "no_copy": 0,
- "options": "Member",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "member",
+ "fieldtype": "Link",
+ "label": "Member",
+ "options": "Member"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "membership_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Membership Type",
- "length": 0,
- "no_copy": 0,
- "options": "Membership Type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "membership_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Membership Type",
+ "options": "Membership Type",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "membership_status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Membership Status",
- "length": 0,
- "no_copy": 0,
- "options": "New\nCurrent\nExpired\nPending\nCancelled",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "membership_status",
+ "fieldtype": "Select",
+ "label": "Membership Status",
+ "options": "New\nCurrent\nExpired\nPending\nCancelled"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "membership_validity_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Validity",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "membership_validity_section",
+ "fieldtype": "Section Break",
+ "label": "Validity"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "From",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "From",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "To",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "To",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_8",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "member_since_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Member Since",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "member_since_date",
+ "fieldtype": "Date",
+ "label": "Member Since"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_details",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "payment_details",
+ "fieldtype": "Section Break",
+ "label": "Payment Details"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "paid",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Paid",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "paid",
+ "fieldtype": "Check",
+ "label": "Paid"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "currency",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "USD\nINR",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "currency",
+ "fieldtype": "Select",
+ "label": "Currency",
+ "options": "USD\nINR"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "amount",
+ "fieldtype": "Float",
+ "label": "Amount"
+ },
+ {
+ "fieldname": "razorpay_details_section",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Razorpay Details"
+ },
+ {
+ "fieldname": "subscription_id",
+ "fieldtype": "Data",
+ "label": "Subscription ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "payment_id",
+ "fieldtype": "Data",
+ "label": "Payment ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "webhook_payload",
+ "fieldtype": "Code",
+ "label": "Webhook Payload",
+ "options": "JSON",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:42.323446",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2020-04-06 14:29:33.856060",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Membership",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Member",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Member",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "restrict_to_domain": "Non Profit",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index 98bee56979f..a523a238e4b 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -3,9 +3,13 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+import json
import frappe
+import six
+from datetime import datetime
from frappe.model.document import Document
-from frappe.utils import add_days, add_years, nowdate, getdate
+from frappe.email import sendmail_to_system_managers
+from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form
from frappe import _
import erpnext
@@ -43,11 +47,80 @@ class Membership(Document):
else:
self.from_date = nowdate()
- self.to_date = add_years(self.from_date, 1)
+ if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly":
+ self.to_date = add_years(self.from_date, 1)
+ else:
+ self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None):
if status_changed_to in ("Completed", "Authorized"):
self.load_from_db()
self.db_set('paid', 1)
+def get_member_based_on_subscription(subscription_id, email):
+ members = frappe.get_all("Member", filters={
+ 'subscription_id': subscription_id,
+ 'email_id': email
+ }, order_by="creation desc")
+ return frappe.get_doc("Member", members[0]['name'])
+
+
+@frappe.whitelist()
+def trigger_razorpay_subscription(data):
+ if isinstance(data, six.string_types):
+ data = json.loads(data)
+ data = frappe._dict(data)
+
+ subscription = data.payload.get("subscription", {}).get('entity', {})
+ subscription = frappe._dict(subscription)
+
+ payment = data.payload.get("payment", {}).get('entity', {})
+ payment = frappe._dict(payment)
+
+ try:
+ data_json = json.dumps(data, indent=4, sort_keys=True)
+ member = get_member_based_on_subscription(subscription.id, payment.email)
+ except Exception as e:
+ error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed"))
+ notify_failure(error_log)
+ raise e
+
+ if data.event == "subscription.activated":
+ member.customer_id = payment.customer_id
+ member.subscription_start = datetime.fromtimestamp(subscription.start_at)
+ member.subscription_end = datetime.fromtimestamp(subscription.end_at)
+ member.subscription_activated = 1
+ member.save(ignore_permissions=True)
+ elif data.event == "subscription.charged":
+ membership = frappe.new_doc("Membership")
+ membership.update({
+ "member": member.name,
+ "membership_status": "Current",
+ "membership_type": member.membership_type,
+ "currency": "INR",
+ "paid": 1,
+ "payment_id": payment.id,
+ "webhook_payload": data_json,
+ "from_date": datetime.fromtimestamp(subscription.current_start),
+ "to_date": datetime.fromtimestamp(subscription.current_end),
+ "amount": payment.amount / 100 # Convert to rupees from paise
+ })
+ membership.insert(ignore_permissions=True)
+
+ return True
+
+
+
+def notify_failure(log):
+ try:
+ content = """Dear System Manager,
+Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
+
+Error Log: {0}
+
+Regards,
+Administrator""".format(get_link_to_form("Error Log", log.name))
+ sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
+ except:
+ pass
diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/non_profit/doctype/membership_settings/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
new file mode 100644
index 00000000000..c01a0b23d5d
--- /dev/null
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Membership Settings', {
+ refresh: function(frm) {
+
+ }
+});
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
new file mode 100644
index 00000000000..56b8eac4b12
--- /dev/null
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
@@ -0,0 +1,62 @@
+{
+ "actions": [],
+ "creation": "2020-03-29 12:57:03.005120",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable_razorpay",
+ "razorpay_settings_section",
+ "billing_cycle",
+ "billing_frequency"
+ ],
+ "fields": [
+ {
+ "fieldname": "billing_cycle",
+ "fieldtype": "Select",
+ "label": "Billing Cycle",
+ "options": "Monthly\nYearly"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_razorpay",
+ "fieldtype": "Check",
+ "label": "Enable RazorPay For Memberships"
+ },
+ {
+ "depends_on": "eval:doc.enable_razorpay",
+ "fieldname": "razorpay_settings_section",
+ "fieldtype": "Section Break",
+ "label": "RazorPay Settings"
+ },
+ {
+ "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
+ "fieldname": "billing_frequency",
+ "fieldtype": "Int",
+ "label": "Billing Frequency"
+ }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-04-07 18:42:51.496807",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Membership Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py
new file mode 100644
index 00000000000..2b8e37f2a65
--- /dev/null
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.py
@@ -0,0 +1,17 @@
+# -*- 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.integrations.utils import get_payment_gateway_controller
+from frappe.model.document import Document
+
+class MembershipSettings(Document):
+ pass
+
+@frappe.whitelist()
+def get_plans_for_membership(*args, **kwargs):
+ controller = get_payment_gateway_controller("Razorpay")
+ plans = controller.get_plans()
+ return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
new file mode 100644
index 00000000000..2ad7984583d
--- /dev/null
+++ b/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
@@ -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 TestMembershipSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 3ef39aee7d5..226981dc783 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.js
@@ -2,7 +2,9 @@
// For license information, please see license.txt
frappe.ui.form.on('Membership Type', {
- refresh: function() {
-
+ refresh: function(frm) {
+ frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
+ })
}
});
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json
index 35a7902c9f7..319078fd6c3 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.json
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.json
@@ -1,124 +1,63 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:membership_type",
- "beta": 0,
- "creation": "2017-09-18 12:56:56.343999",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "field:membership_type",
+ "creation": "2017-09-18 12:56:56.343999",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "membership_type",
+ "amount",
+ "razorpay_plan_id"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "membership_type",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Membership Type",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "membership_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Membership Type",
+ "reqd": 1,
+ "unique": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "amount",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "razorpay_plan_id",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Razorpay Plan ID",
+ "unique": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-05 07:03:45.860757",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership Type",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2020-03-30 12:54:07.850857",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Membership Type",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Non Profit Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Non Profit",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "restrict_to_domain": "Non Profit",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/set_permission_einvoicing.py b/erpnext/patches/v12_0/set_permission_einvoicing.py
index 1095c8c43e7..e2235105f94 100644
--- a/erpnext/patches/v12_0/set_permission_einvoicing.py
+++ b/erpnext/patches/v12_0/set_permission_einvoicing.py
@@ -10,6 +10,8 @@ def execute():
make_custom_fields()
+ frappe.reload_doc("regional", "doctype", "import_supplier_invoice")
+
add_permission('Import Supplier Invoice', 'Accounts Manager', 0)
update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'write', 1)
- update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1)
\ No newline at end of file
+ update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1)
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 49abab1c131..f3cecd9059b 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -83,7 +83,6 @@
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Open\nCompleted\nCancelled",
- "reqd": 1,
"search_index": 1
},
{
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 1490374d2d2..e94d1ffe5c0 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -1,18 +1,18 @@
{
- "css/erpnext.css": [
- "public/less/erpnext.less",
- "public/less/hub.less",
- "public/less/call_popup.less"
- ],
- "css/marketplace.css": [
- "public/less/hub.less"
- ],
- "js/erpnext-web.min.js": [
- "public/js/website_utils.js",
- "public/js/shopping_cart.js"
- ],
+ "css/erpnext.css": [
+ "public/less/erpnext.less",
+ "public/less/hub.less",
+ "public/less/call_popup.less"
+ ],
+ "css/marketplace.css": [
+ "public/less/hub.less"
+ ],
+ "js/erpnext-web.min.js": [
+ "public/js/website_utils.js",
+ "public/js/shopping_cart.js"
+ ],
"css/erpnext-web.css": [
- "public/scss/website.scss"
+ "public/scss/website.scss"
],
"js/marketplace.min.js": [
"public/js/hub/marketplace.js"
@@ -47,15 +47,15 @@
"public/js/templates/item_quick_entry.html",
"public/js/utils/item_quick_entry.js",
"public/js/utils/customer_quick_entry.js",
- "public/js/education/student_button.html",
- "public/js/education/assessment_result_tool.html",
- "public/js/hub/hub_factory.js",
- "public/js/call_popup/call_popup.js",
- "public/js/utils/dimension_tree_filter.js"
- ],
- "js/item-dashboard.min.js": [
- "stock/dashboard/item_dashboard.html",
- "stock/dashboard/item_dashboard_list.html",
- "stock/dashboard/item_dashboard.js"
- ]
+ "public/js/education/student_button.html",
+ "public/js/education/assessment_result_tool.html",
+ "public/js/hub/hub_factory.js",
+ "public/js/call_popup/call_popup.js",
+ "public/js/utils/dimension_tree_filter.js"
+ ],
+ "js/item-dashboard.min.js": [
+ "stock/dashboard/item_dashboard.html",
+ "stock/dashboard/item_dashboard_list.html",
+ "stock/dashboard/item_dashboard.js"
+ ]
}
diff --git a/erpnext/public/css/erpnext.css b/erpnext/public/css/erpnext.css
index c55e4221515..6e4efcb6685 100644
--- a/erpnext/public/css/erpnext.css
+++ b/erpnext/public/css/erpnext.css
@@ -370,3 +370,39 @@ body[data-route="pos"] .collapse-btn {
.leaderboard .list-item_content {
padding-right: 45px;
}
+.exercise-card {
+ box-shadow: 0 1px 3px rgba(0,0,0,0.30);
+ border-radius: 2px;
+ padding: 6px 6px 6px 8px;
+ margin-top: 10px;
+ height: 100% !important;
+}
+.exercise-card .card-img-top {
+ width: 100%;
+ height: 15vw;
+ object-fit: cover;
+}
+.exercise-card .btn-edit {
+ position: absolute;
+ bottom: 10px;
+ left: 20px;
+}
+.exercise-card .btn-del {
+ position: absolute;
+ bottom: 10px;
+ left: 50px;
+}
+.exercise-card .card-body {
+ margin-bottom: 10px;
+}
+.exercise-card .card-footer {
+ padding: 10px;
+}
+.exercise-row {
+ height: 100% !important;
+ display: flex;
+ flex-wrap: wrap;
+}
+.exercise-col {
+ padding: 10px;
+}
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index afbdbc661d3..d5dc412e7d0 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -444,75 +444,69 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
"fieldname": "quantity",
"reqd": 1,
"default": 1
- },
- {
- "fieldtype": "Button",
- "label": __("Get Items"),
- "fieldname": "get_items",
- "cssClass": "btn-primary"
}
- ]
- });
-
- dialog.fields_dict.get_items.$input.click(function() {
- var args = dialog.get_values();
- if(!args) return;
- dialog.hide();
- return frappe.call({
- type: "GET",
- method: "erpnext.stock.doctype.packed_item.packed_item.get_items_from_product_bundle",
- args: {
+ ],
+ primary_action_label: 'Get Items',
+ primary_action(args){
+ if(!args) return;
+ dialog.hide();
+ return frappe.call({
+ type: "GET",
+ method: "erpnext.stock.doctype.packed_item.packed_item.get_items_from_product_bundle",
args: {
- item_code: args.product_bundle,
- quantity: args.quantity,
- parenttype: frm.doc.doctype,
- parent: frm.doc.name,
- supplier: frm.doc.supplier,
- currency: frm.doc.currency,
- conversion_rate: frm.doc.conversion_rate,
- price_list: frm.doc.buying_price_list,
- price_list_currency: frm.doc.price_list_currency,
- plc_conversion_rate: frm.doc.plc_conversion_rate,
- company: frm.doc.company,
- is_subcontracted: frm.doc.is_subcontracted,
- transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
- ignore_pricing_rule: frm.doc.ignore_pricing_rule,
- doctype: frm.doc.doctype
- }
- },
- freeze: true,
- callback: function(r) {
- const first_row_is_empty = function(child_table){
- if($.isArray(child_table) && child_table.length > 0) {
- return !child_table[0].item_code;
+ args: {
+ item_code: args.product_bundle,
+ quantity: args.quantity,
+ parenttype: frm.doc.doctype,
+ parent: frm.doc.name,
+ supplier: frm.doc.supplier,
+ currency: frm.doc.currency,
+ conversion_rate: frm.doc.conversion_rate,
+ price_list: frm.doc.buying_price_list,
+ price_list_currency: frm.doc.price_list_currency,
+ plc_conversion_rate: frm.doc.plc_conversion_rate,
+ company: frm.doc.company,
+ is_subcontracted: frm.doc.is_subcontracted,
+ transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
+ ignore_pricing_rule: frm.doc.ignore_pricing_rule,
+ doctype: frm.doc.doctype
}
- return false;
- };
+ },
+ freeze: true,
+ callback: function(r) {
+ const first_row_is_empty = function(child_table){
+ if($.isArray(child_table) && child_table.length > 0) {
+ return !child_table[0].item_code;
+ }
+ return false;
+ };
- const remove_empty_first_row = function(frm){
- if (first_row_is_empty(frm.doc.items)){
- frm.doc.items = frm.doc.items.splice(1);
- }
- };
+ const remove_empty_first_row = function(frm){
+ if (first_row_is_empty(frm.doc.items)){
+ frm.doc.items = frm.doc.items.splice(1);
+ }
+ };
- if(!r.exc && r.message) {
- remove_empty_first_row(frm);
- for ( var i=0; i< r.message.length; i++ ) {
- var d = frm.add_child("items");
- var item = r.message[i];
- for ( var key in item) {
- if ( !is_null(item[key]) ) {
- d[key] = item[key];
+ if(!r.exc && r.message) {
+ remove_empty_first_row(frm);
+ for ( var i=0; i< r.message.length; i++ ) {
+ var d = frm.add_child("items");
+ var item = r.message[i];
+ for ( var key in item) {
+ if ( !is_null(item[key]) ) {
+ d[key] = item[key];
+ }
+ }
+ if(frappe.meta.get_docfield(d.doctype, "price_list_rate", d.name)) {
+ frm.script_manager.trigger("price_list_rate", d.doctype, d.name);
}
}
- if(frappe.meta.get_docfield(d.doctype, "price_list_rate", d.name)) {
- frm.script_manager.trigger("price_list_rate", d.doctype, d.name);
- }
+ frm.refresh_field("items");
}
- frm.refresh_field("items");
}
- }
- })
+ })
+ }
});
+
dialog.show();
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 8e8c48feb0d..3443abc7344 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -499,7 +499,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
weight_uom: item.weight_uom,
- uom : item.uom,
manufacturer: item.manufacturer,
stock_uom: item.stock_uom,
pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
@@ -551,6 +550,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!d[k]) d[k] = v;
});
+ if (d.has_batch_no && d.has_serial_no) {
+ d.batch_no = undefined;
+ }
+
erpnext.show_serial_batch_selector(me.frm, d, (item) => {
me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse)
@@ -1430,6 +1433,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
for (let key in free_item_data) {
row_to_modify[key] = free_item_data[key];
}
+ } if (items && items.length && free_item_data) {
+ items[0].qty = free_item_data.qty
}
},
diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less
index abe48685f03..8685837d33d 100644
--- a/erpnext/public/less/erpnext.less
+++ b/erpnext/public/less/erpnext.less
@@ -458,4 +458,50 @@ body[data-route="pos"] {
.list-item_content {
padding-right: 45px;
}
+}
+
+// Healthcare
+
+.exercise-card {
+ box-shadow: 0 1px 3px rgba(0,0,0,0.30);
+ border-radius: 2px;
+ padding: 6px 6px 6px 8px;
+ margin-top: 10px;
+ height: 100% !important;
+
+ .card-img-top {
+ width: 100%;
+ height: 15vw;
+ object-fit: cover;
+ }
+
+ .btn-edit {
+ position: absolute;
+ bottom: 10px;
+ left: 20px;
+ }
+
+ .btn-del {
+ position: absolute;
+ bottom: 10px;
+ left: 50px;
+ }
+
+ .card-body {
+ margin-bottom: 10px;
+ }
+
+ .card-footer {
+ padding: 10px;
+ }
+}
+
+.exercise-row {
+ height: 100% !important;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.exercise-col {
+ padding: 10px;
}
\ No newline at end of file
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 28b1f8ffb8b..4be6804db56 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -85,7 +85,7 @@ def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
allow_on_submit=1, print_hide=1, fetch_if_empty=1)
- nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is nil rated or exempted',
+ nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted',
fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code',
print_hide=1)
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
@@ -388,7 +388,7 @@ def make_custom_fields(update=True):
'Item': [
dict(fieldname='gst_hsn_code', label='HSN/SAC',
fieldtype='Link', options='GST HSN Code', insert_after='item_group'),
- dict(fieldname='is_nil_exempt', label='Is nil rated or exempted',
+ dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted',
fieldtype='Check', insert_after='gst_hsn_code'),
dict(fieldname='is_non_gst', label='Is Non GST ',
fieldtype='Check', insert_after='is_nil_exempt')
@@ -497,6 +497,14 @@ def make_custom_fields(update=True):
'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
+ ],
+ "Member": [
+ {
+ 'fieldname': 'pan_number',
+ 'label': 'PAN Details',
+ 'fieldtype': 'Data',
+ 'insert_after': 'email'
+ }
]
}
create_custom_fields(custom_fields, update=update)
@@ -718,4 +726,4 @@ def get_tds_details(accounts, fiscal_year):
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}])
- ]
+ ]
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 54e87f7a3b5..6462d3bc8c3 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -60,9 +60,9 @@
"base_total",
"base_net_total",
"column_break_33",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_38",
@@ -1196,7 +1196,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-30 19:15:28.605085",
+ "modified": "2020-04-17 12:50:39.640534",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js
index f175687f26c..7011cf9804b 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.js
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.js
@@ -287,7 +287,7 @@ erpnext.pos.PointOfSale = class PointOfSale {
if (in_list(['serial_no', 'batch_no'], field)) {
args[field] = value;
}
-
+
// add to cur_frm
const item = this.frm.add_child('items', args);
frappe.flags.hide_serial_batch_dialog = true;
diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
index 28dd0564075..aa57665a815 100644
--- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
+++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
@@ -53,10 +53,11 @@ def execute(filters=None):
new[1], repeat[1], new[1] + repeat[1]])
return [
- _("Year"), _("Month"),
- _("New Customers") + ":Int",
- _("Repeat Customers") + ":Int",
- _("Total") + ":Int",
+ _("Year") + "::100",
+ _("Month") + "::100",
+ _("New Customers") + ":Int:100",
+ _("Repeat Customers") + ":Int:100",
+ _("Total") + ":Int:100",
_("New Customer Revenue") + ":Currency:150",
_("Repeat Customer Revenue") + ":Currency:150",
_("Total Revenue") + ":Currency:150"
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index af100692c6f..095b7c3dffa 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -228,9 +228,15 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
warehouse: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
+
+ if (item.serial_no && item.qty === item.serial_no.split(`\n`).length) {
+ return;
+ }
+
if (item.serial_no && !item.batch_no) {
item.serial_no = null;
}
+
var has_batch_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
has_batch_no = r && r.has_batch_no;
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 6f9d83d6749..9f5dee901ce 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -66,9 +66,9 @@
"base_total",
"base_net_total",
"column_break_33",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_39",
@@ -1256,7 +1256,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-31 19:17:13.122644",
+ "modified": "2020-04-17 12:51:41.288600",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 47a72b21a69..d7a93fb6917 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -434,15 +434,6 @@ class TestDeliveryNote(unittest.TestCase):
update_delivery_note_status(dn.name, "Closed")
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
- def test_customer_provided_parts_dn(self):
- create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- dn = create_delivery_note(item_code='CUST-0987', rate=0)
- self.assertEqual(dn.get("items")[0].allow_zero_valuation_rate, 1)
-
- # test if Delivery Note with rate is allowed against Customer Provided Item
- dn2 = create_delivery_note(item_code='CUST-0987', do_not_save=True)
- self.assertRaises(frappe.ValidationError, dn2.save)
-
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order()
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 6110ea822cf..b97da693b41 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -12,7 +12,8 @@ frappe.ui.form.on('Material Request', {
'Purchase Order': 'Purchase Order',
'Request for Quotation': 'Request for Quotation',
'Supplier Quotation': 'Supplier Quotation',
- 'Work Order': 'Work Order'
+ 'Work Order': 'Work Order',
+ 'Purchase Receipt': 'Purchase Receipt'
};
// formatter for material request item
@@ -27,11 +28,20 @@ frappe.ui.form.on('Material Request', {
// set schedule_date
set_schedule_date(frm);
- frm.fields_dict["items"].grid.get_field("warehouse").get_query = function(doc) {
+
+ let filters = {'company': frm.doc.company}
+
+ frm.set_query("warehouse", "items", function() {
return {
- filters: {'company': doc.company}
+ filters: filters
};
- };
+ });
+
+ frm.set_query("set_warehouse", function(){
+ return {
+ filters: filters
+ };
+ });
},
onload_post_render: function(frm) {
@@ -129,12 +139,13 @@ frappe.ui.form.on('Material Request', {
source_doctype: "Sales Order",
target: frm,
setters: {
- company: frm.doc.company
+ customer: frm.doc.customer || undefined
},
get_query_filters: {
docstatus: 1,
status: ["not in", ["Closed", "On Hold"]],
per_delivered: ["<", 99.99],
+ company: frm.doc.company
}
});
},
@@ -182,46 +193,46 @@ frappe.ui.form.on('Material Request', {
options:"BOM", reqd: 1, get_query: function() {
return {filters: { docstatus:1 }};
}},
- {"fieldname":"warehouse", "fieldtype":"Link", "label":__("Warehouse"),
+ {"fieldname":"warehouse", "fieldtype":"Link", "label":__("For Warehouse"),
options:"Warehouse", reqd: 1},
{"fieldname":"qty", "fieldtype":"Float", "label":__("Quantity"),
reqd: 1, "default": 1},
{"fieldname":"fetch_exploded", "fieldtype":"Check",
- "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1},
- {fieldname:"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"}
- ]
- });
- d.get_input("fetch").on("click", function() {
- var values = d.get_values();
- if(!values) return;
- values["company"] = frm.doc.company;
- if(!frm.doc.company) frappe.throw(__("Company field is required"));
- frappe.call({
- method: "erpnext.manufacturing.doctype.bom.bom.get_bom_items",
- args: values,
- callback: function(r) {
- if (!r.message) {
- frappe.throw(__("BOM does not contain any stock item"));
- } else {
- erpnext.utils.remove_empty_first_row(frm, "items");
- $.each(r.message, function(i, item) {
- var d = frappe.model.add_child(cur_frm.doc, "Material Request Item", "items");
- d.item_code = item.item_code;
- d.item_name = item.item_name;
- d.description = item.description;
- d.warehouse = values.warehouse;
- d.uom = item.stock_uom;
- d.stock_uom = item.stock_uom;
- d.conversion_factor = 1;
- d.qty = item.qty;
- d.project = item.project;
- });
+ "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}
+ ],
+ primary_action_label: 'Get Items',
+ primary_action(values) {
+ if(!values) return;
+ values["company"] = frm.doc.company;
+ if(!frm.doc.company) frappe.throw(__("Company field is required"));
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom.bom.get_bom_items",
+ args: values,
+ callback: function(r) {
+ if (!r.message) {
+ frappe.throw(__("BOM does not contain any stock item"));
+ } else {
+ erpnext.utils.remove_empty_first_row(frm, "items");
+ $.each(r.message, function(i, item) {
+ var d = frappe.model.add_child(cur_frm.doc, "Material Request Item", "items");
+ d.item_code = item.item_code;
+ d.item_name = item.item_name;
+ d.description = item.description;
+ d.warehouse = values.warehouse;
+ d.uom = item.stock_uom;
+ d.stock_uom = item.stock_uom;
+ d.conversion_factor = 1;
+ d.qty = item.qty;
+ d.project = item.project;
+ });
+ }
+ d.hide();
+ refresh_field("items");
}
- d.hide();
- refresh_field("items");
- }
- });
+ });
+ }
});
+
d.show();
},
@@ -248,7 +259,8 @@ frappe.ui.form.on('Material Request', {
run_link_triggers: true
});
},
- __('Enter Supplier')
+ __('Enter Supplier'),
+ __('Create')
)
},
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index d0025d12ab0..536f5fa0ac5 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -1,9 +1,11 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-03-07 14:48:38",
"doctype": "DocType",
"document_type": "Document",
+ "engine": "InnoDB",
"field_order": [
"type_section",
"naming_series",
@@ -14,6 +16,8 @@
"schedule_date",
"company",
"amended_from",
+ "warehouse_section",
+ "set_warehouse",
"items_section",
"scan_barcode",
"items",
@@ -66,7 +70,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
- "label": "Type",
+ "label": "Purpose",
"options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided",
"reqd": 1
},
@@ -85,7 +89,7 @@
"allow_on_submit": 1,
"fieldname": "schedule_date",
"fieldtype": "Date",
- "label": "Required Date"
+ "label": "Required By"
},
{
"fieldname": "company",
@@ -190,6 +194,7 @@
"width": "100px"
},
{
+ "depends_on": "eval:doc.docstatus==1",
"fieldname": "per_ordered",
"fieldtype": "Percent",
"label": "% Ordered",
@@ -200,6 +205,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.docstatus==1",
"fieldname": "per_received",
"fieldtype": "Percent",
"label": "% Received",
@@ -270,12 +276,24 @@
"options": "Job Card",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "description": "Sets 'For Warehouse' in each row of the Items table.",
+ "fieldname": "set_warehouse",
+ "fieldtype": "Link",
+ "label": "Set Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-ticket",
"idx": 70,
"is_submittable": 1,
- "modified": "2019-04-29 11:45:07.570292",
+ "links": [],
+ "modified": "2020-03-02 20:21:09.990867",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 5b242a51bc8..2d9855713c3 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -175,12 +175,11 @@ class MaterialRequest(BuyingController):
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
- target_ref_field = 'qty' if self.material_request_type == "Manufacture" else 'stock_qty'
self._update_percent_field({
"target_dt": "Material Request Item",
"target_parent_dt": self.doctype,
"target_parent_field": "per_ordered",
- "target_ref_field": target_ref_field,
+ "target_ref_field": "stock_qty",
"target_field": "ordered_qty",
"name": self.name,
}, update_modified)
@@ -499,7 +498,7 @@ def raise_work_orders(material_request):
default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
for d in mr.items:
- if (d.qty - d.ordered_qty) >0:
+ if (d.stock_qty - d.ordered_qty) > 0:
if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}):
wo_order = frappe.new_doc("Work Order")
wo_order.update({
@@ -531,7 +530,7 @@ def raise_work_orders(material_request):
msgprint(_("The following Work Orders were created:") + '\n' + new_line_sep(message))
if errors:
- frappe.throw(_("Productions Orders cannot be raised for:") + '\n' + new_line_sep(errors))
+ frappe.throw(_("Work Order cannot be created for following reason:") + '\n' + new_line_sep(errors))
return work_orders
diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py
index cbd64784c63..0e4fb7a6dd0 100644
--- a/erpnext/stock/doctype/material_request/material_request_dashboard.py
+++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py
@@ -8,7 +8,12 @@ def get_data():
'transactions': [
{
'label': _('Related'),
- 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order', 'Stock Entry', 'Pick List']
+ 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order']
+ },
+ {
+ 'label': _('Stock'),
+ 'items': ['Stock Entry', 'Purchase Receipt', 'Pick List']
+
},
{
'label': _('Manufacturing'),
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index b925aedd1ac..19924b16363 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -7,7 +7,8 @@
from __future__ import unicode_literals
import frappe, unittest, erpnext
from frappe.utils import flt, today
-from erpnext.stock.doctype.material_request.material_request import raise_work_orders
+from erpnext.stock.doctype.material_request.material_request \
+ import raise_work_orders, make_stock_entry, make_purchase_order, make_supplier_quotation
from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase):
@@ -15,8 +16,6 @@ class TestMaterialRequest(unittest.TestCase):
erpnext.set_perpetual_inventory(0)
def test_make_purchase_order(self):
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
-
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_purchase_order,
@@ -30,8 +29,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(len(po.get("items")), len(mr.get("items")))
def test_make_supplier_quotation(self):
- from erpnext.stock.doctype.material_request.material_request import make_supplier_quotation
-
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name)
@@ -45,12 +42,9 @@ class TestMaterialRequest(unittest.TestCase):
def test_make_stock_entry(self):
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
mr = frappe.copy_doc(test_records[0]).insert()
- self.assertRaises(frappe.ValidationError, make_stock_entry,
- mr.name)
+ self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.material_request_type = "Material Transfer"
@@ -62,40 +56,40 @@ class TestMaterialRequest(unittest.TestCase):
def _insert_stock_entry(self, qty1, qty2, warehouse = None ):
se = frappe.get_doc({
- "company": "_Test Company",
- "doctype": "Stock Entry",
- "posting_date": "2013-03-01",
- "posting_time": "00:00:00",
- "purpose": "Material Receipt",
- "items": [
- {
- "conversion_factor": 1.0,
- "doctype": "Stock Entry Detail",
- "item_code": "_Test Item Home Desktop 100",
- "parentfield": "items",
- "basic_rate": 100,
- "qty": qty1,
- "stock_uom": "_Test UOM 1",
- "transfer_qty": qty1,
- "uom": "_Test UOM 1",
- "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": "_Test Cost Center - _TC"
- },
- {
- "conversion_factor": 1.0,
- "doctype": "Stock Entry Detail",
- "item_code": "_Test Item Home Desktop 200",
- "parentfield": "items",
- "basic_rate": 100,
- "qty": qty2,
- "stock_uom": "_Test UOM 1",
- "transfer_qty": qty2,
- "uom": "_Test UOM 1",
- "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": "_Test Cost Center - _TC"
- }
- ]
- })
+ "company": "_Test Company",
+ "doctype": "Stock Entry",
+ "posting_date": "2013-03-01",
+ "posting_time": "00:00:00",
+ "purpose": "Material Receipt",
+ "items": [
+ {
+ "conversion_factor": 1.0,
+ "doctype": "Stock Entry Detail",
+ "item_code": "_Test Item Home Desktop 100",
+ "parentfield": "items",
+ "basic_rate": 100,
+ "qty": qty1,
+ "stock_uom": "_Test UOM 1",
+ "transfer_qty": qty1,
+ "uom": "_Test UOM 1",
+ "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ },
+ {
+ "conversion_factor": 1.0,
+ "doctype": "Stock Entry Detail",
+ "item_code": "_Test Item Home Desktop 200",
+ "parentfield": "items",
+ "basic_rate": 100,
+ "qty": qty2,
+ "stock_uom": "_Test UOM 1",
+ "transfer_qty": qty2,
+ "uom": "_Test UOM 1",
+ "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ }
+ ]
+ })
se.set_stock_entry_type()
se.insert()
@@ -198,14 +192,7 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # check if per complete is None
- mr.load_from_db()
- self.assertEqual(mr.per_ordered, 0)
- self.assertEqual(mr.get("items")[0].ordered_qty, 0)
- self.assertEqual(mr.get("items")[1].ordered_qty, 0)
-
# map a purchase order
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
po_doc = make_purchase_order(mr.name)
po_doc.supplier = "_Test Supplier"
po_doc.transaction_date = "2013-07-07"
@@ -276,10 +263,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0)
-
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
# map a stock entry
se_doc = make_stock_entry(mr.name)
@@ -331,8 +316,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 27.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 1.5)
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
# check if per complete is as expected for Stock Entry cancelled
se.cancel()
@@ -344,8 +329,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0)
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_completed_qty_for_over_transfer(self):
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
@@ -357,14 +342,7 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # check if per complete is None
- mr.load_from_db()
- self.assertEqual(mr.per_ordered, 0)
- self.assertEqual(mr.get("items")[0].ordered_qty, 0)
- self.assertEqual(mr.get("items")[1].ordered_qty, 0)
-
# map a stock entry
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name)
se_doc.update({
@@ -425,8 +403,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0)
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_incorrect_mapping_of_stock_entry(self):
# submit material request of type Transfer
@@ -435,9 +413,6 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # map a stock entry
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
se_doc = make_stock_entry(mr.name)
se_doc.update({
"posting_date": "2013-03-01",
@@ -468,8 +443,6 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # map a stock entry
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name)
self.assertEqual(se_doc.get("items")[0].s_warehouse, "_Test Warehouse - _TC")
@@ -483,8 +456,6 @@ class TestMaterialRequest(unittest.TestCase):
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty"))
def test_make_stock_entry_for_material_issue(self):
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_stock_entry,
@@ -503,8 +474,6 @@ class TestMaterialRequest(unittest.TestCase):
return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100",
"warehouse": "_Test Warehouse - _TC"}, "indented_qty"))
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
existing_requested_qty = _get_requested_qty()
mr = frappe.copy_doc(test_records[0])
@@ -563,9 +532,37 @@ class TestMaterialRequest(unittest.TestCase):
item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0]
self.assertEqual(requested_qty, new_requested_qty)
- def test_multi_uom_for_purchase(self):
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+ def test_requested_qty_multi_uom(self):
+ existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
+ uom="_Test UOM 1", conversion_factor=12)
+
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+
+ self.assertEqual(requested_qty, existing_requested_qty + 120)
+
+ work_order = raise_work_orders(mr.name)
+ wo = frappe.get_doc("Work Order", work_order[0])
+ wo.qty = 50
+ wo.wip_warehouse = "_Test Warehouse 1 - _TC"
+ wo.submit()
+
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ self.assertEqual(requested_qty, existing_requested_qty + 70)
+
+ wo.cancel()
+
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ self.assertEqual(requested_qty, existing_requested_qty + 120)
+
+ mr.reload()
+ mr.cancel()
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ self.assertEqual(requested_qty, existing_requested_qty)
+
+
+ def test_multi_uom_for_purchase(self):
mr = frappe.copy_doc(test_records[0])
mr.material_request_type = 'Purchase'
item = mr.items[0]
@@ -607,7 +604,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(mr.per_ordered, 100)
def test_customer_provided_parts_mr(self):
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
@@ -633,6 +629,8 @@ def make_material_request(**args):
mr.append("items", {
"item_code": args.item_code or "_Test Item",
"qty": args.qty or 10,
+ "uom": args.uom or "_Test UOM",
+ "conversion_factor": args.conversion_factor or 1,
"schedule_date": args.schedule_date or today(),
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC"
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index 30206b62d0c..2bdc268c78a 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -1,5 +1,4 @@
{
- "actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:28:02",
"doctype": "DocType",
@@ -8,36 +7,38 @@
"engine": "InnoDB",
"field_order": [
"item_code",
- "col_break1",
"item_name",
+ "col_break1",
+ "schedule_date",
"section_break_4",
"description",
"item_group",
"brand",
"image_section",
"image",
- "manufacture_details",
- "manufacturer",
"column_break_12",
"manufacturer_part_no",
"quantity_and_warehouse",
"qty",
- "uom",
- "conversion_factor",
"stock_uom",
"warehouse",
"col_break2",
- "schedule_date",
- "rate",
- "amount",
+ "uom",
+ "conversion_factor",
"stock_qty",
+ "rate_and_amount_section_break",
+ "rate",
+ "col_break3",
+ "amount",
+ "manufacture_details",
+ "manufacturer",
"more_info",
"lead_time_date",
"sales_order",
"sales_order_item",
"production_plan",
"material_request_plan_item",
- "col_break3",
+ "col_break4",
"min_order_qty",
"projected_qty",
"actual_qty",
@@ -176,7 +177,7 @@
"fieldname": "schedule_date",
"fieldtype": "Date",
"in_list_view": 1,
- "label": "Required Date",
+ "label": "Required By",
"oldfieldname": "schedule_date",
"oldfieldtype": "Date",
"print_width": "100px",
@@ -186,14 +187,12 @@
{
"fieldname": "rate",
"fieldtype": "Currency",
- "label": "Rate",
- "no_copy": 1
+ "label": "Rate"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
- "no_copy": 1,
"read_only": 1
},
{
@@ -205,6 +204,7 @@
"read_only": 1
},
{
+ "collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information"
@@ -332,6 +332,7 @@
"read_only": 1
},
{
+ "collapsible": 1,
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
@@ -374,7 +375,10 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
- "label": "Received Quantity"
+ "label": "Received Quantity",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
},
{
"collapsible": 1,
@@ -387,6 +391,7 @@
"fieldtype": "Column Break"
},
{
+ "collapsible": 1,
"fieldname": "manufacture_details",
"fieldtype": "Section Break",
"label": "Manufacture"
@@ -404,13 +409,22 @@
{
"fieldname": "manufacturer_part_no",
"fieldtype": "Data",
- "label": "Manufacturer Part Number"
+ "label": "Manufacturer Part Number",
+ "read_only": 1
+ },
+ {
+ "fieldname": "rate_and_amount_section_break",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "col_break4",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-07 18:37:54.495112",
+ "modified": "2020-04-16 09:00:00.992835",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 6b4f73b140a..1b9ff41cc33 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -111,6 +111,7 @@ class TestPickList(unittest.TestCase):
stock_reconciliation = frappe.get_doc({
'doctype': 'Stock Reconciliation',
+ 'purpose': 'Stock Reconciliation',
'company': '_Test Company',
'items': [{
'item_code': '_Test Serialized Item',
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index e38bb38b195..467a206d188 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -16,10 +16,10 @@
"supplier_name",
"supplier_delivery_note",
"column_break1",
+ "company",
"posting_date",
"posting_time",
"set_posting_time",
- "company",
"is_return",
"return_against",
"section_addresses",
@@ -49,19 +49,19 @@
"items_section",
"scan_barcode",
"items",
- "pricing_rule_details",
- "pricing_rules",
- "get_current_stock",
- "raw_material_details",
- "supplied_items",
"section_break0",
"total_qty",
"base_total",
"base_net_total",
"column_break_27",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
+ "pricing_rule_details",
+ "pricing_rules",
+ "raw_material_details",
+ "get_current_stock",
+ "supplied_items",
"taxes_charges_section",
"tax_category",
"shipping_col",
@@ -113,13 +113,13 @@
"auto_repeat",
"printing_settings",
"letter_head",
- "select_print_heading",
"language",
- "group_same_items",
- "column_break_97",
- "other_details",
"instructions",
+ "column_break_97",
+ "select_print_heading",
+ "other_details",
"remarks",
+ "group_same_items",
"transporter_info",
"transporter_name",
"column_break5",
@@ -404,6 +404,7 @@
"fieldtype": "Section Break"
},
{
+ "description": "Sets 'Accepted Warehouse' in each row of the items table.",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"label": "Accepted Warehouse",
@@ -411,7 +412,7 @@
"print_hide": 1
},
{
- "description": "Warehouse where you are maintaining stock of rejected items",
+ "description": "Sets 'Rejected Warehouse' in each row of the items table.",
"fieldname": "rejected_warehouse",
"fieldtype": "Link",
"label": "Rejected Warehouse",
@@ -429,7 +430,7 @@
"default": "No",
"fieldname": "is_subcontracted",
"fieldtype": "Select",
- "label": "Raw Materials Supplied",
+ "label": "Raw Materials Consumed",
"oldfieldname": "is_subcontracted",
"oldfieldtype": "Select",
"options": "No\nYes",
@@ -465,6 +466,7 @@
"reqd": 1
},
{
+ "collapsible": 1,
"fieldname": "pricing_rule_details",
"fieldtype": "Section Break",
"label": "Pricing Rules"
@@ -477,9 +479,10 @@
"read_only": 1
},
{
+ "depends_on": "supplied_items",
"fieldname": "get_current_stock",
"fieldtype": "Button",
- "label": "Get current stock",
+ "label": "Get Current Stock",
"oldfieldtype": "Button",
"options": "get_current_stock",
"print_hide": 1
@@ -489,7 +492,7 @@
"collapsible_depends_on": "supplied_items",
"fieldname": "raw_material_details",
"fieldtype": "Section Break",
- "label": "Raw Materials Supplied",
+ "label": "Raw Materials Consumed",
"oldfieldtype": "Section Break",
"options": "fa fa-table",
"print_hide": 1,
@@ -498,7 +501,7 @@
{
"fieldname": "supplied_items",
"fieldtype": "Table",
- "label": "Supplied Items",
+ "label": "Consumed Items",
"no_copy": 1,
"oldfieldname": "pr_raw_material_details",
"oldfieldtype": "Table",
@@ -1082,7 +1085,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-06 16:31:37.444891",
+ "modified": "2020-04-18 18:02:18.020763",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
@@ -1149,4 +1152,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 113da9f4218..40d7cc2537c 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -356,8 +356,8 @@ class TestPurchaseReceipt(unittest.TestCase):
'accounts': [{
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
- 'accumulated_depreciation_account': 'Depreciation - _TC',
- 'depreciation_expense_account': 'Depreciation - _TC'
+ 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
+ 'depreciation_expense_account': '_Test Depreciation - _TC'
}]
}).insert()
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index b15f23c3038..bc6bce95d6e 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -15,15 +15,11 @@
"item_name",
"section_break_4",
"description",
- "item_group",
"brand",
- "image_section",
+ "image_column",
+ "item_group",
"image",
"image_view",
- "manufacture_details",
- "manufacturer",
- "column_break_16",
- "manufacturer_part_no",
"received_and_accepted",
"received_qty",
"qty",
@@ -32,6 +28,7 @@
"uom",
"stock_uom",
"conversion_factor",
+ "stock_qty",
"retain_sample",
"sample_quantity",
"rate_and_amount",
@@ -60,11 +57,6 @@
"rm_supp_cost",
"landed_cost_voucher_amount",
"billed_amt",
- "item_weight_details",
- "weight_per_unit",
- "total_weight",
- "column_break_41",
- "weight_uom",
"warehouse_and_reference",
"warehouse",
"rejected_warehouse",
@@ -77,20 +69,27 @@
"asset_category",
"schedule_date",
"quality_inspection",
- "stock_qty",
"purchase_order_item",
"material_request_item",
"section_break_45",
"allow_zero_valuation_rate",
"bom",
- "col_break5",
"serial_no",
+ "col_break5",
+ "include_exploded_items",
"batch_no",
- "column_break_48",
"rejected_serial_no",
"expense_account",
- "include_exploded_items",
"item_tax_rate",
+ "item_weight_details",
+ "weight_per_unit",
+ "total_weight",
+ "column_break_41",
+ "weight_uom",
+ "manufacture_details",
+ "manufacturer",
+ "column_break_16",
+ "manufacturer_part_no",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -526,7 +525,7 @@
{
"fieldname": "stock_qty",
"fieldtype": "Float",
- "label": "Qty as per Stock UOM",
+ "label": "Accepted Qty as per Stock UOM",
"oldfieldname": "stock_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -553,17 +552,13 @@
"fieldname": "batch_no",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "Batch No!",
+ "label": "Batch No",
"no_copy": 1,
"oldfieldname": "batch_no",
"oldfieldtype": "Link",
"options": "Batch",
"print_hide": 1
},
- {
- "fieldname": "column_break_48",
- "fieldtype": "Column Break"
- },
{
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "rejected_serial_no",
@@ -658,6 +653,7 @@
"read_only": 1
},
{
+ "fetch_from": "item_code.brand",
"fieldname": "brand",
"fieldtype": "Link",
"hidden": 1,
@@ -669,9 +665,9 @@
"read_only": 1
},
{
+ "fetch_from": "item_code.item_group",
"fieldname": "item_group",
"fieldtype": "Link",
- "hidden": 1,
"label": "Item Group",
"oldfieldname": "item_group",
"oldfieldtype": "Link",
@@ -748,22 +744,19 @@
"fieldname": "section_break_80",
"fieldtype": "Section Break"
},
- {
- "collapsible": 1,
- "fieldname": "image_section",
- "fieldtype": "Section Break",
- "label": "Image"
- },
{
"fieldname": "material_request",
"fieldtype": "Link",
"label": "Material Request",
- "options": "Material Request"
+ "options": "Material Request",
+ "read_only": 1
},
{
"fieldname": "material_request_item",
"fieldtype": "Data",
- "label": "Material Request Item"
+ "hidden": 1,
+ "label": "Material Request Item",
+ "read_only": 1
},
{
"fieldname": "expense_account",
@@ -826,12 +819,17 @@
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"options": "Warehouse"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "image_column",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-07 18:38:21.141558",
+ "modified": "2020-04-10 19:01:21.154963",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 7cf822bf49f..95f9d4633b5 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -865,14 +865,6 @@ class StockEntry(StockController):
self.add_to_stock_entry_detail(item_dict)
- if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
- scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty)
- for item in itervalues(scrap_item_dict):
- if self.pro_doc and self.pro_doc.scrap_warehouse:
- item["to_warehouse"] = self.pro_doc.scrap_warehouse
-
- self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no)
-
# fetch the serial_no of the first stock entry for the second stock entry
if self.work_order and self.purpose == "Manufacture":
self.set_serial_nos(self.work_order)
@@ -883,9 +875,20 @@ class StockEntry(StockController):
if self.purpose in ("Manufacture", "Repack"):
self.load_items_from_bom()
+ self.set_scrap_items()
self.set_actual_qty()
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
+ def set_scrap_items(self):
+ if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
+ scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty)
+ for item in itervalues(scrap_item_dict):
+ item.idx = ''
+ if self.pro_doc and self.pro_doc.scrap_warehouse:
+ item["to_warehouse"] = self.pro_doc.scrap_warehouse
+
+ self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no)
+
def set_work_order_details(self):
if not getattr(self, "pro_doc", None):
self.pro_doc = frappe._dict()
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index 7f4efba33f8..b7d1497319f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -4,6 +4,7 @@
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
"doctype": "DocType",
"document_type": "Document",
+ "engine": "InnoDB",
"field_order": [
"naming_series",
"company",
@@ -44,11 +45,11 @@
"reqd": 1
},
{
- "default": "Stock Reconciliation",
"fieldname": "purpose",
"fieldtype": "Select",
"label": "Purpose",
- "options": "Opening Stock\nStock Reconciliation"
+ "options": "\nOpening Stock\nStock Reconciliation",
+ "reqd": 1
},
{
"fieldname": "col1",
@@ -153,7 +154,7 @@
"idx": 1,
"is_submittable": 1,
"max_attachments": 1,
- "modified": "2019-05-26 09:03:09.542141",
+ "modified": "2020-04-08 17:02:47.196206",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index e6d7e3fea7d..51d027f22ef 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -240,6 +240,7 @@ def create_batch_or_serial_no_items():
def create_stock_reconciliation(**args):
args = frappe._dict(args)
sr = frappe.new_doc("Stock Reconciliation")
+ sr.purpose = args.purpose or "Stock Reconciliation"
sr.posting_date = args.posting_date or nowdate()
sr.posting_time = args.posting_time or nowtime()
sr.set_posting_time = 1
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 97776739a87..4c721acdc12 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
import erpnext
+import json
from frappe.utils import flt, nowdate, add_days, cint
from frappe import _
@@ -198,19 +199,16 @@ def send_email_notification(mr_list):
subject=_('Auto Material Requests Generated'), message = msg)
def notify_errors(exceptions_list):
- subject = "[Important] [ERPNext] Auto Reorder Errors"
- content = """Dear System Manager,
+ subject = _("[Important] [ERPNext] Auto Reorder Errors")
+ content = _("Dear System Manager,") + "
" + _("An error occured for certain Items while creating Material Requests based on Re-order level. \
+ Please rectify these issues :") + "
"
-An error occured for certain Items while creating Material Requests based on Re-order level.
+ for exception in exceptions_list:
+ exception = json.loads(exception)
+ error_message = """
{0}
""".format(_(exception.get("message")))
+ content += error_message
-Please rectify these issues:
----
-
-%s
-
----
-Regards,
-Administrator""" % ("\n\n".join(exceptions_list),)
+ content += _("Regards,") + "
" + _("Administrator")
from frappe.email import sendmail_to_system_managers
sendmail_to_system_managers(subject, content)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 2bdb04ed2c9..56973153609 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -113,24 +113,30 @@ def get_reserved_qty(item_code, warehouse):
return flt(reserved_qty[0][0]) if reserved_qty else 0
def get_indented_qty(item_code, warehouse):
- inward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor)
- from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
- where mr_item.item_code=%s and mr_item.warehouse=%s
- and mr.material_request_type in ('Purchase', 'Manufacture')
- and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name
- and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
-
- outward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor)
+ # Ordered Qty is always maintained in stock UOM
+ inward_qty = frappe.db.sql("""
+ select sum(mr_item.stock_qty - mr_item.ordered_qty)
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
where mr_item.item_code=%s and mr_item.warehouse=%s
- and mr.material_request_type in ('Material Issue', 'Material Transfer')
- and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name
- and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
+ and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer')
+ and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
+ and mr.status!='Stopped' and mr.docstatus=1
+ """, (item_code, warehouse))
+ inward_qty = flt(inward_qty[0][0]) if inward_qty else 0
- inward_qty, outward_qty = flt(inward_qty[0][0]) if inward_qty else 0, flt(outward_qty[0][0]) if outward_qty else 0
- indented_qty = inward_qty - outward_qty
+ outward_qty = frappe.db.sql("""
+ select sum(mr_item.stock_qty - mr_item.ordered_qty)
+ from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
+ where mr_item.item_code=%s and mr_item.warehouse=%s
+ and mr.material_request_type = 'Material Issue'
+ and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
+ and mr.status!='Stopped' and mr.docstatus=1
+ """, (item_code, warehouse))
+ outward_qty = flt(outward_qty[0][0]) if outward_qty else 0
- return indented_qty
+ requested_qty = inward_qty - outward_qty
+
+ return requested_qty
def get_ordered_qty(item_code, warehouse):
ordered_qty = frappe.db.sql("""
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index fd72807418f..117267f1a42 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -335,8 +335,13 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
ignore_permissions = False
if is_website_user():
- if not filters: filters = []
- filters.append(("Issue", "customer", "=", customer)) if customer else filters.append(("Issue", "raised_by", "=", user))
+ if not filters: filters = {}
+
+ if customer:
+ filters["customer"] = customer
+ else:
+ filters["raised_by"] = user
+
ignore_permissions = True
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index f88ffd44e3c..14674c067cd 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -126,7 +126,7 @@ class TransactionBase(StatusUpdater):
frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ")
.format(d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate))
frappe.throw(_("To allow different rates, disable the {0} checkbox in {1}.")
- .format(frappe.bold("Maintain Same Rate Throughout Sales Cycle"),
+ .format(frappe.bold(_("Maintain Same Rate Throughout Sales Cycle")),
get_link_to_form("Selling Settings", "Selling Settings", frappe.bold("Selling Settings"))))
def get_link_filters(self, for_doctype):
@@ -179,4 +179,6 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
qty = d.get(f)
if qty:
if abs(cint(qty) - flt(qty)) > 0.0000001:
- frappe.throw(_("Quantity ({0}) cannot be a fraction in row {1}").format(qty, d.idx), UOMMustBeIntegerError)
+ frappe.throw(_("Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.") \
+ .format(qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))),
+ UOMMustBeIntegerError)