From e927224fbc45b54929825b015ad7da161bc984d9 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 4 Nov 2020 19:57:47 +0530 Subject: [PATCH 001/114] feat: update membership setting doctype * rename enable_auto_invoicing to enable_invoicing * add option to make_payment entry --- .../membership_settings.json | 42 +++++++++++++------ .../membership_type/membership_type.js | 4 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 5b6bab5b0a..a70c3c4b8a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,9 +11,11 @@ "billing_frequency", "webhook_secret", "column_break_6", - "enable_auto_invoicing", + "enable_invoicing", + "make_payment_entry", "company", "debit_account", + "payment_account", "column_break_9", "send_email", "send_invoice", @@ -58,14 +60,7 @@ "label": "Invoicing" }, { - "default": "0", - "fieldname": "enable_auto_invoicing", - "fieldtype": "Check", - "label": "Enable Auto Invoicing", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.enable_auto_invoicing", + "depends_on": "eval:doc.enable_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", @@ -77,7 +72,7 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.enable_auto_invoicing", + "depends_on": "eval:doc.enable_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -86,7 +81,7 @@ }, { "default": "0", - "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", + "depends_on": "eval:doc.enable_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", "label": "Send Invoice with Email" @@ -119,11 +114,34 @@ "label": "Email Template", "mandatory_depends_on": "eval:doc.send_email", "options": "Email Template" + }, + { + "default": "0", + "fieldname": "enable_invoicing", + "fieldtype": "Check", + "label": "Enable Invoicing", + "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" + }, + { + "default": "0", + "depends_on": "eval:doc.enable_invoicing", + "fieldname": "make_payment_entry", + "fieldtype": "Check", + "label": "Make Payment Entry" + }, + { + "depends_on": "eval:doc.make_payment_entry", + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Payment To", + "mandatory_depends_on": "eval:doc.make_payment_entry", + "options": "Account" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-05 17:26:37.287395", + "modified": "2020-11-04 19:51:21.990595", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 43311a2c96..94ccdd8334 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -2,12 +2,12 @@ // For license information, please see license.txt frappe.ui.form.on('Membership Type', { - refresh: function(frm) { + 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); }); - frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + frappe.db.get_single_value("Membership Settings", "enable_invoicing").then(val => { if (val) frm.set_df_property('linked_item', 'hidden', false); }); } From d75ff1a93e562ac5e22dc5afd2aef20fc8c62ab1 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 4 Nov 2020 20:17:33 +0530 Subject: [PATCH 002/114] feat: generate invoice on payment authorized --- erpnext/non_profit/doctype/membership/membership.py | 11 ++++++++--- .../membership_settings/membership_settings.json | 10 +++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 4c85cb60e8..97de63b052 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -54,9 +54,14 @@ class Membership(Document): 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) + if status_changed_to not in ("Completed", "Authorized"): + return + self.load_from_db() + self.db_set('paid', 1) + settings = frappe.get_doc("Membership Settings") + if settings.enable_invoicing and settings.create_for_web_forms: + self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) + def generate_invoice(self, save=True): if not (self.paid or self.currency or self.amount): diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index a70c3c4b8a..a25f5ffbc2 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -12,6 +12,7 @@ "webhook_secret", "column_break_6", "enable_invoicing", + "create_for_web_forms", "make_payment_entry", "company", "debit_account", @@ -136,12 +137,19 @@ "label": "Payment To", "mandatory_depends_on": "eval:doc.make_payment_entry", "options": "Account" + }, + { + "depends_on": "eval:doc.enable_invoicing", + "description": "Automatically create an invoice when payment is authorized from a web form entry", + "fieldname": "create_for_web_forms", + "fieldtype": "Data", + "label": "Auto Create Invoice for Web Forms" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-04 19:51:21.990595", + "modified": "2020-11-04 20:19:55.163749", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 9d9fa74e6b8dd5db1f89bc6bf92809c0fba29eda Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 12:29:03 +0530 Subject: [PATCH 003/114] refactor(member): drop email column * remove email column * update controller methods * add patch to add value from email to email_id --- erpnext/non_profit/doctype/member/member.json | 10 +--------- .../doctype/membership/membership.py | 8 ++++++-- erpnext/patches.txt | 3 ++- .../v13_0/update_member_email_address.py | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 erpnext/patches/v13_0/update_member_email_address.py diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index 992ef16d64..f190cfae75 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -12,7 +12,6 @@ "membership_expiry_date", "column_break_5", "membership_type", - "email", "email_id", "image", "customer_section", @@ -64,13 +63,6 @@ "options": "Membership Type", "reqd": 1 }, - { - "fieldname": "email", - "fieldtype": "Link", - "in_list_view": 1, - "label": "User", - "options": "User" - }, { "fieldname": "image", "fieldtype": "Attach Image", @@ -178,7 +170,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-09-16 23:44:13.596948", + "modified": "2020-11-09 12:12:10.174647", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 97de63b052..36f68bc00c 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -24,7 +24,7 @@ class Membership(Document): user = frappe.get_doc('User', frappe.session.user) member = frappe.get_doc(dict( doctype='Member', - email=frappe.session.user, + email_id=frappe.session.user, membership_type=self.membership_type, member_name=user.get_fullname() )).insert(ignore_permissions=True) @@ -97,8 +97,12 @@ class Membership(Document): frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) member = frappe.get_doc("Member", self.member) + + if not member.email_id: + frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) + plan = frappe.get_doc("Membership Type", self.membership_type) - email = member.email_id if member.email_id else member.email + email = member.email_id attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] if self.invoice and settings.send_invoice: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 34dbdd0bd5..a9cd25fd42 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -733,4 +733,5 @@ erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee -execute:frappe.delete_doc("Report", "Quoted Item Comparison") \ No newline at end of file +execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.update_member_email_address \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py new file mode 100644 index 0000000000..da7828adbc --- /dev/null +++ b/erpnext/patches/v13_0/update_member_email_address.py @@ -0,0 +1,19 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + """add value to email_id column from email""" + + if frappe.db.has_column("Member", "email"): + # Get all members + for member in frappe.db.get_all("Member", pluck="name"): + # Check if email_id already exists + if not frappe.db.get_value("Member", member, "email_id"): + # fetch email id from the user linked field email + email = frappe.db.get_value("Member", member, "email") + + # Set the value for it + frappe.db.set_value("Member", member, "email_id", email) From e0f4dd0643a9ef59d81d70d35050f7e51cfcdc1d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 12:29:27 +0530 Subject: [PATCH 004/114] fix: fieldtype for auto_create_for_web_forms --- .../doctype/membership_settings/membership_settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index a25f5ffbc2..961a9b9b3b 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -139,17 +139,18 @@ "options": "Account" }, { + "default": "0", "depends_on": "eval:doc.enable_invoicing", "description": "Automatically create an invoice when payment is authorized from a web form entry", "fieldname": "create_for_web_forms", - "fieldtype": "Data", + "fieldtype": "Check", "label": "Auto Create Invoice for Web Forms" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-04 20:19:55.163749", + "modified": "2020-11-09 12:28:49.972434", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 286ec04197e6cad7aac9a95d9c8996bc44006252 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 12:53:00 +0530 Subject: [PATCH 005/114] test(membership): setup test defaults --- .../doctype/membership/membership.py | 2 + .../doctype/membership/test_membership.py | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 36f68bc00c..ae4df4a374 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -162,6 +162,8 @@ def get_member_based_on_subscription(subscription_id, email): return None def verify_signature(data): + if frappe.flags.in_test: + return True signature = frappe.request.headers.get('X-Razorpay-Signature') settings = frappe.get_doc("Membership Settings") diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index b23f4062a9..b62f19bd0d 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -2,8 +2,51 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - import unittest +from erpnext.non_profit.doctype.member.member import create_member +from erpnext.stock.doctype.item.test_item import create_item class TestMembership(unittest.TestCase): - pass + def setUp(self): + # Get default company + company = frappe.get_doc("Company", erpnext.get_default_company()) + + # update membership settings + settings = frappe.get_doc("Membership Settings") + # Enable razorpay + settings.enable_razorpay = 1 + settings.billing_cycle = "Monthly" + settings.billing_frequency = 24 + # Enable invoicing + settings.enable_invoicing = 1 + settings.make_payment_entry = 1 + settings.company = company.name + settings.payment_to = company.default_cash_account + settings.debit_account = company.default_receivable_account + settings.save() + + # make test plan + plan = frappe.new_doc("Membership Type") + plan.amount = 100 + plan.razorpay_plan_id = "_rzpy_test_milythm" + plan.linked_item = create_item("_Test Item for Non Profit Membership") + plan.insert() + + # make test member + self.member_doc = create_member(frappe._dict({ + 'fullname': "_Test_Member", + 'email': "_test_member_erpnext@example.com", + 'plan_id': plan.name + })) + + def test_auto_generate_invoice_and_payment_entry(self): + pass + + def test_renew within_30_days(self): + pass + + def test_from_to_dates(self): + pass + + def test_razorpay_webook(self): + pass From c04321e64586ec7f466bb848530712320f4bfbe8 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 13:29:46 +0530 Subject: [PATCH 006/114] test(membership): add test for invoicing and validation --- .../doctype/membership/membership.py | 16 +++- .../doctype/membership/test_membership.py | 78 ++++++++++++++++--- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index ae4df4a374..ac3b89a8d0 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -86,6 +86,20 @@ class Membership(Document): invoice = make_invoice(self, member, plan, settings) self.invoice = invoice.name + if with_payment_entry: + if not settings.payment_account: + frappe.throw(_("You need to set Payment Account in Membership Settings")) + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + frappe.flags.ignore_account_permission=True + pe = get_payment_entry(dt='Sales Invoice', dn=invoice.name, bank_amount=invoice.grand_total) + frappe.flags.ignore_account_permission=False + pe.paid_to = settings.payment_account + pe.reference_no = self.name + pe.reference_date = getdate() + pe.save(ignore_permissions=True) + pe.submit() + if save: self.save() @@ -97,7 +111,7 @@ class Membership(Document): frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) member = frappe.get_doc("Member", self.member) - + if not member.email_id: frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index b62f19bd0d..6e4885d013 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -3,7 +3,10 @@ # See license.txt from __future__ import unicode_literals import unittest +import frappe +import erpnext from erpnext.non_profit.doctype.member.member import create_member +from frappe.utils import nowdate, getdate, add_months from erpnext.stock.doctype.item.test_item import create_item class TestMembership(unittest.TestCase): @@ -21,15 +24,16 @@ class TestMembership(unittest.TestCase): settings.enable_invoicing = 1 settings.make_payment_entry = 1 settings.company = company.name - settings.payment_to = company.default_cash_account + settings.payment_account = company.default_cash_account settings.debit_account = company.default_receivable_account settings.save() # make test plan plan = frappe.new_doc("Membership Type") + plan.membership_type = "_rzpy_test_milythm" plan.amount = 100 plan.razorpay_plan_id = "_rzpy_test_milythm" - plan.linked_item = create_item("_Test Item for Non Profit Membership") + plan.linked_item = create_item("_Test Item for Non Profit Membership").name plan.insert() # make test member @@ -38,15 +42,71 @@ class TestMembership(unittest.TestCase): 'email': "_test_member_erpnext@example.com", 'plan_id': plan.name })) + self.member_doc.make_customer_and_link() + self.member = "self.member_doc.name" def test_auto_generate_invoice_and_payment_entry(self): - pass + entry = make_membership(self.member) - def test_renew within_30_days(self): - pass + # Naive test to see if at all invoice was generated and attached to member + # In any case if details were missing, the invoicing would throw an error + invoice = entry.generate_invoice(save=True) + self.assertEqual(invoice.name, entry.invoice) + # entry.delete() - def test_from_to_dates(self): - pass + # # Remove customer + # old_customer = self.member_doc.customer + # self.member_doc.customer = None + # self.member_doc.save() - def test_razorpay_webook(self): - pass + # entry = make_membership(self.member) + # self.assertRaises(frappe.ValidationError, entry.generate_invoice) + + # # Add customer value back + # self.member_doc.customer = old_customer + # self.member_doc.save() + + # # Remove company + # set_config(company, None) + # self.assertRaises(frappe.ValidationError, entry.generate_invoice) + + def test_renew_within_30_days(self): + # create a membership for two months + # Should work fine + make_membership(self.member, { "from_date": nowdate() }) + make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) + + from frappe.utils.user import add_role + add_role("test@example.com", "Non Profit Manager") + frappe.set_user("test@example.com") + + # create next membership with expiry not within 30 days + self.assertRaises(frappe.ValidationError, make_membership, self.member, { + "from_date": add_months(nowdate(), 2), + }) + + frappe.set_user("Administrator") + # create the same membership but as administrator + new_entry = make_membership(self.member, { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3), + }) + +def set_config(key, value): + frappe.db.set_value("Membership Settings", None, key, value) + +def make_membership(member, payload={}): + data = { + "doctype": "Membership", + "member": member, + "membership_status": "Current", + "membership_type": "_rzpy_test_milythm", + "currency": "INR", + "paid": 1, + "from_date": nowdate(), + "amount": 100 + } + data.update(payload) + membership = frappe.get_doc(data) + membership.insert(ignore_permissions=True, ignore_if_duplicate=True) + return membership \ No newline at end of file From 7e1cdf9b978ffdb6713a2e2cade4ac7307b73533 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 14:01:11 +0530 Subject: [PATCH 007/114] feat(breaking): update get_last_membership to fetch correct details --- erpnext/__init__.py | 14 ++++---------- .../non_profit/doctype/membership/membership.py | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 38d8a62f07..5a5c448026 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -132,16 +132,10 @@ def allow_regional(fn): return caller -def get_last_membership(): +def get_last_membership(member): '''Returns last membership if exists''' last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', - dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) + dict(member=member, paid=1), order_by='to_date desc', limit=1) - return last_membership and last_membership[0] - -def is_member(): - '''Returns true if the user is still a member''' - last_membership = get_last_membership() - if last_membership and getdate(last_membership.to_date) > getdate(): - return True - return False + if last_membership: + return last_membership[0] diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index ac3b89a8d0..7c83a4e0da 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -34,7 +34,7 @@ class Membership(Document): self.member = member_name # get last membership (if active) - last_membership = erpnext.get_last_membership() + last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership if last_membership and not frappe.session.user == "Administrator": From 12fafa3e7a20bb8a2ad54ea43f8e3d2146bd30b5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 14:01:22 +0530 Subject: [PATCH 008/114] chore: remove validation for old member field --- erpnext/non_profit/doctype/member/member.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 44b975e9e9..7fc4f225aa 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -18,8 +18,6 @@ class Member(Document): def validate(self): - if self.email: - self.validate_email_type(self.email) if self.email_id: self.validate_email_type(self.email_id) From 723e220a3409150de11c4b74a7bbb5911060382b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 14:01:52 +0530 Subject: [PATCH 009/114] chore: remove commented code --- .../doctype/membership/test_membership.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index 6e4885d013..ce31b91956 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -52,23 +52,6 @@ class TestMembership(unittest.TestCase): # In any case if details were missing, the invoicing would throw an error invoice = entry.generate_invoice(save=True) self.assertEqual(invoice.name, entry.invoice) - # entry.delete() - - # # Remove customer - # old_customer = self.member_doc.customer - # self.member_doc.customer = None - # self.member_doc.save() - - # entry = make_membership(self.member) - # self.assertRaises(frappe.ValidationError, entry.generate_invoice) - - # # Add customer value back - # self.member_doc.customer = old_customer - # self.member_doc.save() - - # # Remove company - # set_config(company, None) - # self.assertRaises(frappe.ValidationError, entry.generate_invoice) def test_renew_within_30_days(self): # create a membership for two months From c1b0e65f9f9df5fdad4a813a788f4f458b2e8318 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 12 Nov 2020 09:54:44 +0530 Subject: [PATCH 010/114] feat: Putaway --- .../stock/doctype/putaway_rule/__init__.py | 0 .../doctype/putaway_rule/putaway_rule.js | 18 +++ .../doctype/putaway_rule/putaway_rule.json | 111 ++++++++++++++++++ .../doctype/putaway_rule/putaway_rule.py | 93 +++++++++++++++ .../doctype/putaway_rule/putaway_rule_list.js | 10 ++ .../doctype/putaway_rule/test_putaway_rule.py | 10 ++ 6 files changed, 242 insertions(+) create mode 100644 erpnext/stock/doctype/putaway_rule/__init__.py create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.js create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.json create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.py create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule_list.js create mode 100644 erpnext/stock/doctype/putaway_rule/test_putaway_rule.py diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js new file mode 100644 index 0000000000..ae08e82c28 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -0,0 +1,18 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Putaway Rule', { + setup: function(frm) { + frm.set_query("warehouse", function() { + return { + "filters": { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + } + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json new file mode 100644 index 0000000000..6a132c7e25 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "autoname": "format:{item_code}-{warehouse}", + "creation": "2020-11-09 11:39:46.489501", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disable", + "item_code", + "item_name", + "warehouse", + "col_break_capacity", + "company", + "capacity", + "priority", + "stock_uom" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Item", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "col_break_capacity", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capacity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Capacity", + "reqd": 1 + }, + { + "default": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-10 17:06:27.151335", + "modified_by": "Administrator", + "module": "Stock", + "name": "Putaway Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py new file mode 100644 index 0000000000..9f02833431 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -0,0 +1,93 @@ +# -*- 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 copy +from frappe import _ +from frappe.utils import flt +from frappe.model.document import Document + +class PutawayRule(Document): + def validate(self): + self.validate_duplicate_rule() + self.validate_warehouse_and_company() + self.validate_capacity() + self.validate_priority() + + def validate_duplicate_rule(self): + existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + if existing_rule and existing_rule != self.name: + frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") + .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), + title=_("Duplicate")) + + def validate_priority(self): + if self.priority < 1: + frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority")) + + def validate_warehouse_and_company(self): + company = frappe.db.get_value("Warehouse", self.warehouse, "company") + if company != self.company: + frappe.throw(_("Warehouse {0} does not belong to Company {1}.") + .format(frappe.bold(self.warehouse), frappe.bold(self.company)), + title=_("Invalid Warehouse")) + + def validate_capacity(self): + # check if capacity is lesser than current balance in warehouse + pass + +@frappe.whitelist() +def get_ordered_putaway_rules(item_code, company, qty): + """Returns an ordered list of putaway rules to apply on an item.""" + + # get enabled putaway rules for this item code in this company that have pending capacity + # order the rules by priority first + # if same priority, order by pending capacity (capacity - get how much stock is in the warehouse) + # return this list + # [{'name': "something", "free space": 20}, {'name': "something", "free space": 10}] + +@frappe.whitelist() +def apply_putaway_rule(items, company): + """ Applies Putaway Rule on line items. + + items: List of line items in a Purchase Receipt + company: Company in Purchase Receipt + """ + items_not_accomodated = [] + for item in items: + item_qty = item.qty + at_capacity, rules = get_ordered_putaway_rules(item.item_code, company, item_qty) + + if not rules: + if at_capacity: + items_not_accomodated.append([item.item_code, item_qty]) + continue + + item_row_updated = False + for rule in rules: + while item_qty > 0: + if not item_row_updated: + # update pre-existing row + item.qty = rule.qty + item.warehouse = rule.warehouse + item_row_updated = True + else: + # add rows for split quantity + added_row = copy.deepcopy(item) + added_row.qty = rule.qty + added_row.warehouse = rule.warehouse + items.append(added_row) + + item_qty -= flt(rule.qty) + + # if pending qty after applying rules, add row without warehouse + if item_qty > 0: + added_row = copy.deepcopy(item) + added_row.qty = item_qty + added_row.warehouse = '' + items.append(added_row) + items_not_accomodated.append([item.item_code, item_qty]) + + # need to check pricing rule, item tax impact \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js new file mode 100644 index 0000000000..bb1654cf24 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Putaway Rule'] = { + add_fields: ["disable"], + get_indicator: (doc) => { + if (doc.disable) { + return [__("Disabled"), "darkgrey", "disable,=,1"]; + } else { + return [__("Active"), "blue", "disable,=,0"]; + }; + } +}; diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py new file mode 100644 index 0000000000..e262217f84 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.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 TestPutawayRule(unittest.TestCase): + pass From c7991f85612a0b3b608e942ec593d9c980b5c302 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 20 Nov 2020 11:53:20 +0530 Subject: [PATCH 011/114] feat: Putaway Rule --- .../doctype/purchase_order/purchase_order.py | 2 + .../doctype/putaway_rule/putaway_rule.json | 4 +- .../doctype/putaway_rule/putaway_rule.py | 104 ++++++++++++------ 3 files changed, 76 insertions(+), 34 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c7efb8a1a1..53326fd6b2 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -349,7 +349,9 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] def set_missing_values(source, target): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule target.ignore_pricing_rule = 1 + target.items = apply_putaway_rule(target.items, target.company) target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 6a132c7e25..0d90c47b50 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -55,7 +55,7 @@ "reqd": 1 }, { - "default": "item_code.stock_uom", + "fetch_from": "item_code.stock_uom", "fieldname": "stock_uom", "fieldtype": "Link", "label": "Stock UOM", @@ -86,7 +86,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-10 17:06:27.151335", + "modified": "2020-11-12 11:20:52.765163", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 9f02833431..1ac76b6c30 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -5,9 +5,11 @@ from __future__ import unicode_literals import frappe import copy +from collections import defaultdict from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, nowdate from frappe.model.document import Document +from erpnext.stock.utils import get_stock_balance class PutawayRule(Document): def validate(self): @@ -35,59 +37,97 @@ class PutawayRule(Document): title=_("Invalid Warehouse")) def validate_capacity(self): - # check if capacity is lesser than current balance in warehouse - pass + balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) + if flt(self.capacity) < flt(balance_qty): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") + .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) @frappe.whitelist() -def get_ordered_putaway_rules(item_code, company, qty): +def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" + rules = frappe.get_all("Putaway Rule", fields=["name", "capacity", "priority", "warehouse"], + filters={"item_code": item_code, "company": company, "disable": 0}, + order_by="priority asc, capacity desc") - # get enabled putaway rules for this item code in this company that have pending capacity - # order the rules by priority first - # if same priority, order by pending capacity (capacity - get how much stock is in the warehouse) - # return this list - # [{'name': "something", "free space": 20}, {'name': "something", "free space": 10}] + if not rules: + return False, None + + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.capacity) - flt(balance_qty) + + if free_space > 0: + rule["free_space"] = free_space + else: + del rule + + if not rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + True, None + + rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) + return False, rules @frappe.whitelist() def apply_putaway_rule(items, company): """ Applies Putaway Rule on line items. - items: List of line items in a Purchase Receipt - company: Company in Purchase Receipt + items: List of Purchase Receipt Item objects + company: Company in the Purchase Receipt """ - items_not_accomodated = [] + items_not_accomodated, updated_table = [], [] + item_wise_rules = defaultdict(list) + for item in items: - item_qty = item.qty - at_capacity, rules = get_ordered_putaway_rules(item.item_code, company, item_qty) + item_qty, item_code = flt(item.qty), item.item_code + if not item_qty: continue + + at_capacity, rules = get_ordered_putaway_rules(item_code, company) if not rules: if at_capacity: - items_not_accomodated.append([item.item_code, item_qty]) + items_not_accomodated.append([item_code, item_qty]) continue - item_row_updated = False - for rule in rules: - while item_qty > 0: - if not item_row_updated: - # update pre-existing row - item.qty = rule.qty - item.warehouse = rule.warehouse - item_row_updated = True - else: - # add rows for split quantity - added_row = copy.deepcopy(item) - added_row.qty = rule.qty - added_row.warehouse = rule.warehouse - items.append(added_row) + # maintain item wise rules, to handle if item is entered twice + # in the table, due to different price, etc. + if not item_wise_rules[item_code]: + item_wise_rules[item_code] = rules - item_qty -= flt(rule.qty) + for rule in item_wise_rules[item_code]: + # it gets split if rule has lesser qty + # if rule_qty >= pending_qty => allocate pending_qty in row + # if rule_qty < pending_qty => allocate rule_qty in row and check for next rule + if item_qty > 0 and rule.free_space: + to_allocate = flt(rule.free_space) if item_qty >= flt(rule.free_space) else item_qty + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 + new_updated_table_row.qty = to_allocate + new_updated_table_row.warehouse = rule.warehouse + updated_table.append(new_updated_table_row) + + item_qty -= to_allocate + rule["free_space"] -= to_allocate + if item_qty == 0: break # if pending qty after applying rules, add row without warehouse if item_qty > 0: added_row = copy.deepcopy(item) + added_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 added_row.qty = item_qty added_row.warehouse = '' - items.append(added_row) + updated_table.append(added_row) items_not_accomodated.append([item.item_code, item_qty]) - # need to check pricing rule, item tax impact \ No newline at end of file + if items_not_accomodated: + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

  • " + formatted_item_qty = [entry[0] + " : " + str(entry[1]) for entry in items_not_accomodated] + msg += "
  • ".join(formatted_item_qty) + msg += "
" + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + + return updated_table if updated_table else items + # TODO: check pricing rule, item tax impact \ No newline at end of file From 9596276b95baeaa8fd24be6eb9df8372145aefb5 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 10:32:17 +0530 Subject: [PATCH 012/114] fix: Linter and Sider --- erpnext/buying/doctype/purchase_order/purchase_order.py | 4 ++-- erpnext/stock/doctype/putaway_rule/putaway_rule_list.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 53326fd6b2..bb67eb92c0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted == "Yes": for item in self.items: if not item.bom: - frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ - .format(item.item_code, item.idx))) + frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}") + .format(item.item_code, item.idx)) def get_schedule_dates(self): for d in self.get('items'): diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index bb1654cf24..e48c415f14 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -5,6 +5,6 @@ frappe.listview_settings['Putaway Rule'] = { return [__("Disabled"), "darkgrey", "disable,=,1"]; } else { return [__("Active"), "blue", "disable,=,0"]; - }; + } } }; From 90598ea19cae9f271e7906ee0753cdddb70070c2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 17:35:13 +0530 Subject: [PATCH 013/114] chore: Multi UOM support for Putaway - Added UOM & conversion factor field in Putaway Rule - Items are split and assigned as per UOM - Handled Whole UOMs too --- .../doctype/putaway_rule/putaway_rule.js | 25 ++++++ .../doctype/putaway_rule/putaway_rule.json | 29 ++++++- .../doctype/putaway_rule/putaway_rule.py | 78 +++++++++++-------- 3 files changed, 96 insertions(+), 36 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index ae08e82c28..00a84b0e8d 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -11,7 +11,32 @@ frappe.ui.form.on('Putaway Rule', { } }; }); + }, + + uom: function(frm) { + if(frm.doc.item_code && frm.doc.uom) { + return frm.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { + item_code: frm.doc.item_code, + uom: frm.doc.uom + }, + callback: function(r) { + if(!r.exc) { + let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); + frm.set_value('conversion_factor', r.message.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + } + }); + } + }, + + capacity: function(frm) { + let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); } + // refresh: function(frm) { // } diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 0d90c47b50..d5ae68faf3 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -10,17 +10,19 @@ "item_code", "item_name", "warehouse", + "priority", "col_break_capacity", "company", "capacity", - "priority", - "stock_uom" + "uom", + "conversion_factor", + "stock_uom", + "stock_capacity" ], "fields": [ { "fieldname": "item_code", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Item", "options": "Item", @@ -82,11 +84,30 @@ "fieldname": "disable", "fieldtype": "Check", "label": "Disable" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "stock_capacity", + "fieldtype": "Float", + "label": "Capacity in Stock UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-12 11:20:52.765163", + "modified": "2020-11-23 16:53:48.387054", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 1ac76b6c30..53a947f417 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -7,7 +7,7 @@ import frappe import copy from collections import defaultdict from frappe import _ -from frappe.utils import flt, nowdate +from frappe.utils import flt, floor, nowdate from frappe.model.document import Document from erpnext.stock.utils import get_stock_balance @@ -38,14 +38,14 @@ class PutawayRule(Document): def validate_capacity(self): balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.capacity) < flt(balance_qty): + if flt(self.stock_capacity) < flt(balance_qty): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", fields=["name", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -54,8 +54,7 @@ def get_ordered_putaway_rules(item_code, company): for rule in rules: balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) - free_space = flt(rule.capacity) - flt(balance_qty) - + free_space = flt(rule.stock_capacity) - flt(balance_qty) if free_space > 0: rule["free_space"] = free_space else: @@ -64,7 +63,7 @@ def get_ordered_putaway_rules(item_code, company): if not rules: # After iterating through rules, if no rules are left # then there is not enough space left in any rule - True, None + return True, None rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) return False, rules @@ -79,15 +78,33 @@ def apply_putaway_rule(items, company): items_not_accomodated, updated_table = [], [] item_wise_rules = defaultdict(list) + def add_row(item, to_allocate, warehouse): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 + new_updated_table_row.qty = to_allocate + new_updated_table_row.warehouse = warehouse + updated_table.append(new_updated_table_row) + for item in items: - item_qty, item_code = flt(item.qty), item.item_code - if not item_qty: continue + conversion = flt(item.conversion_factor) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + pending_qty, pending_stock_qty, item_code = flt(item.qty), flt(item.stock_qty), item.item_code + + if not pending_qty: + add_row(item, pending_qty, item.warehouse) + continue at_capacity, rules = get_ordered_putaway_rules(item_code, company) if not rules: if at_capacity: - items_not_accomodated.append([item_code, item_qty]) + # rules available, but no free space + add_row(item, pending_qty, '') + items_not_accomodated.append([item_code, pending_qty]) + else: + # no rules to apply + add_row(item, pending_qty, item.warehouse) continue # maintain item wise rules, to handle if item is entered twice @@ -96,31 +113,28 @@ def apply_putaway_rule(items, company): item_wise_rules[item_code] = rules for rule in item_wise_rules[item_code]: - # it gets split if rule has lesser qty - # if rule_qty >= pending_qty => allocate pending_qty in row - # if rule_qty < pending_qty => allocate rule_qty in row and check for next rule - if item_qty > 0 and rule.free_space: - to_allocate = flt(rule.free_space) if item_qty >= flt(rule.free_space) else item_qty - new_updated_table_row = copy.deepcopy(item) - new_updated_table_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - new_updated_table_row.qty = to_allocate - new_updated_table_row.warehouse = rule.warehouse - updated_table.append(new_updated_table_row) + if pending_stock_qty > 0 and rule.free_space: + stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + qty_to_allocate = stock_qty_to_allocate / (conversion or 1) - item_qty -= to_allocate - rule["free_space"] -= to_allocate - if item_qty == 0: break + if uom_must_be_whole_number: + qty_to_allocate = floor(qty_to_allocate) + stock_qty_to_allocate = qty_to_allocate * conversion - # if pending qty after applying rules, add row without warehouse - if item_qty > 0: - added_row = copy.deepcopy(item) - added_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - added_row.qty = item_qty - added_row.warehouse = '' - updated_table.append(added_row) - items_not_accomodated.append([item.item_code, item_qty]) + if not qty_to_allocate: break + + add_row(item, qty_to_allocate, rule.warehouse) + + pending_stock_qty -= stock_qty_to_allocate + pending_qty -= qty_to_allocate + rule["free_space"] -= stock_qty_to_allocate + + if not pending_stock_qty: break + + # if pending qty after applying all rules, add row without warehouse + if pending_stock_qty > 0: + add_row(item, pending_qty, '') + items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

  • " From 0cec1477f2044f8b09e7f147749fa7b47d9256e9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 19:19:35 +0530 Subject: [PATCH 014/114] chore: Format unassigned Items dialog and add freeze message --- .../doctype/purchase_order/purchase_order.js | 3 ++- .../doctype/putaway_rule/putaway_rule.py | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47483c9d1c..20faded9bb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -347,7 +347,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( make_purchase_receipt: function() { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", - frm: cur_frm + frm: cur_frm, + freeze_message: __("Creating Purchase Receipt ...") }) }, diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 53a947f417..cc58def33a 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -42,6 +42,9 @@ class PutawayRule(Document): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) + if not self.capacity: + frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" @@ -137,10 +140,26 @@ def apply_putaway_rule(items, company): items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    • " - formatted_item_qty = [entry[0] + " : " + str(entry[1]) for entry in items_not_accomodated] - msg += "
    • ".join(formatted_item_qty) - msg += "
    " + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) return updated_table if updated_table else items From ccbd432b56b952e7d40003c15202279379338336 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 12:47:13 +0530 Subject: [PATCH 015/114] chore: Added Tests - Fixed Sider Issues - Added perms to Putaway Rule - Added Unit Tests to check warehouse assignment --- .../doctype/putaway_rule/putaway_rule.js | 4 +- .../doctype/putaway_rule/putaway_rule.json | 27 +- .../doctype/putaway_rule/putaway_rule.py | 7 +- .../doctype/putaway_rule/test_putaway_rule.py | 257 +++++++++++++++++- 4 files changed, 287 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index 00a84b0e8d..e0569206ef 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -14,7 +14,7 @@ frappe.ui.form.on('Putaway Rule', { }, uom: function(frm) { - if(frm.doc.item_code && frm.doc.uom) { + if (frm.doc.item_code && frm.doc.uom) { return frm.call({ method: "erpnext.stock.get_item_details.get_conversion_factor", args: { @@ -22,7 +22,7 @@ frappe.ui.form.on('Putaway Rule', { uom: frm.doc.uom }, callback: function(r) { - if(!r.exc) { + if (!r.exc) { let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); frm.set_value('conversion_factor', r.message.conversion_factor); frm.set_value('stock_capacity', stock_capacity); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index d5ae68faf3..e5b6b2b98f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -107,7 +107,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-23 16:53:48.387054", + "modified": "2020-11-23 19:25:50.948068", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", @@ -121,7 +121,30 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", "share": 1, "write": 1 } diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index cc58def33a..73534aa14f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -17,6 +17,7 @@ class PutawayRule(Document): self.validate_warehouse_and_company() self.validate_capacity() self.validate_priority() + self.set_stock_capacity() def validate_duplicate_rule(self): existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) @@ -45,10 +46,13 @@ class PutawayRule(Document): if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + def set_stock_capacity(self): + self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "stock_capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -86,6 +90,7 @@ def apply_putaway_rule(items, company): new_updated_table_row.name = '' new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 new_updated_table_row.qty = to_allocate + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse updated_table.append(new_updated_table_row) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index e262217f84..7b81784d5f 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -2,9 +2,260 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - -# import frappe +import frappe import unittest +from frappe.utils import add_days, nowdate +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order class TestPutawayRule(unittest.TestCase): - pass + def setUp(self): + if not frappe.db.exists("Item", "_Rice"): + make_item("_Rice", { + 'is_stock_item': 1, + 'has_batch_no' : 1, + 'create_new_batch': 1, + 'stock_uom': 'Kg' + }) + + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): + create_warehouse("Rack 1") + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): + create_warehouse("Rack 2") + + if not frappe.db.exists("UOM", "Bag"): + new_uom = frappe.new_doc("UOM") + new_uom.uom_name = "Bag" + new_uom.save() + + def test_putaway_rules_priority(self): + """Test if rule is applied by priority, irrespective of free space.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, + uom="Kg", priority=2) + + po = create_purchase_order(item_code="_Rice", qty=300) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_2) + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_same_priority(self): + """Test if rule with more free space is applied, + among two rules with same priority and capacity.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=500, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500, + uom="Kg") + + # out of 500 kg capacity, occupy 100 kg in warehouse_1 + stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) + + po = create_purchase_order(item_code="_Rice", qty=700) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 500) + # warehouse_2 has 500 kg free space, it is given priority + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 200) + # warehouse_1 has 400 kg free space, it is given less priority + self.assertEqual(pr.items[1].warehouse, warehouse_1) + + po.cancel() + stock_receipt.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_insufficient_capacity(self): + """Test if qty exceeding capacity, is handled.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=100, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, + uom="Kg") + + po = create_purchase_order(item_code="_Rice", qty=350) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + + self.assertEqual(len(pr.items), 3) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_1) + # extra qty has no warehouse assigned + self.assertEqual(pr.items[2].qty, 50) + self.assertEqual(pr.items[2].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom(self): + """Test rules applied on uom other than stock uom.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=3, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 3000) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=4, + uom="Bag") + self.assertEqual(rule_2.stock_capacity, 4000) + + stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) + + po = create_purchase_order(item_code="_Rice", qty=6, do_not_save=True) + po.items[0].uom = "Bag" + po.save() + po.submit() + + self.assertEqual(po.items[0].stock_qty, 6000) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 4) + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 2) + self.assertEqual(pr.items[1].warehouse, warehouse_1) + + po.cancel() + stock_receipt.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom_whole_uom(self): + """Test if whole UOMs are handled.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) + + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + # Putaway Rule in different UOM + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=1, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 1000) + # Putaway Rule in Stock UOM + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500) + self.assertEqual(rule_2.stock_capacity, 500) + # total capacity is 1500 Kg + + po = create_purchase_order(item_code="_Rice", qty=2, do_not_save=True) + # PO for 2 Bags (2000 Kg) + po.items[0].uom = "Bag" + po.save() + po.submit() + + self.assertEqual(po.items[0].stock_qty, 2000) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 1) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + # leftover space was for 500 kg (0.5 Bag) + # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + self.assertEqual(pr.items[1].qty, 1) + self.assertEqual(pr.items[1].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_reoccurring_item(self): + """Test rules on same item entered multiple times.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=100, + uom="Kg", priority=2) + # total capacity is 300 Kg + + po = create_purchase_order(item_code="_Rice", qty=200, rate=100, do_not_save=True) + po.append("items", { + "item_code":"_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 300, + "rate": 120, + "schedule_date": add_days(nowdate(), 1), + }) + po.save() + po.submit() + # PO for 500 Kg (two rows of same item, different rates) + self.assertEqual(len(po.items), 2) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 3) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + # same rules applied to second item row + # with previous assignment considered + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_2) + # unassigned 200 Kg + self.assertEqual(pr.items[2].qty, 200) + self.assertEqual(pr.items[2].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + +def create_putaway_rule(**args): + args = frappe._dict(args) + putaway = frappe.new_doc("Putaway Rule") + + putaway.disable = args.disable or 0 + putaway.company = args.company or "_Test Company" + putaway.item_code = args.item or args.item_code or "_Test Item" + putaway.warehouse = args.warehouse + putaway.priority = args.priority or 1 + putaway.capacity = args.capacity or 1 + putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") + putaway.uom = args.uom or putaway.stock_uom + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + + if not args.do_not_save: + putaway.save() + + return putaway \ No newline at end of file From 68a49efc8098808386e234c380692791e926b2aa Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 17:38:34 +0530 Subject: [PATCH 016/114] chore: Added Putaway Rule to Desk Page and added Priority to List View --- erpnext/stock/desk_page/stock/stock.json | 4 ++-- erpnext/stock/doctype/putaway_rule/putaway_rule.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 390fcd91e3..aa4fc28ec9 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Putaway Rule\",\n \"name\": \"Putaway Rule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-10-07 18:40:17.130207", + "modified": "2020-11-24 15:43:20.496057", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index e5b6b2b98f..325e6f1355 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -68,6 +68,7 @@ "default": "1", "fieldname": "priority", "fieldtype": "Int", + "in_list_view": 1, "label": "Priority" }, { @@ -107,7 +108,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-23 19:25:50.948068", + "modified": "2020-11-24 16:20:18.306671", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", From 2ed80656aa1be30fb735a85202ce71d62eba6763 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 21:06:43 +0530 Subject: [PATCH 017/114] chore: Code Cleanup - Validate capacity < stock level only on new rule - Mention stock uom while validating capacity in new rule - Separate function to format and display unassigned items - Format ORM args --- .../doctype/putaway_rule/putaway_rule.py | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 73534aa14f..606e190458 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -38,10 +38,13 @@ class PutawayRule(Document): title=_("Invalid Warehouse")) def validate_capacity(self): + stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.stock_capacity) < flt(balance_qty): - frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") - .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) + + if flt(self.stock_capacity) < flt(balance_qty) and self.get('__islocal'): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") + .format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity")) if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) @@ -49,10 +52,10 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) -@frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -145,27 +148,29 @@ def apply_putaway_rule(items, company): items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " - formatted_item_rows = "" - - for entry in items_not_accomodated: - item_link = frappe.utils.get_link_to_form("Item", entry[0]) - formatted_item_rows += """ - {0} - {1} - """.format(item_link, frappe.bold(entry[1])) - - msg += """ - - - - - - {2} -
    {0}{1}
    - """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) - - frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + format_unassigned_items_error(items_not_accomodated) return updated_table if updated_table else items - # TODO: check pricing rule, item tax impact \ No newline at end of file + +def format_unassigned_items_error(items_not_accomodated): + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) \ No newline at end of file From 1087d97c03a0ea9973ffc0d70472c4a3fcac8654 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Nov 2020 10:45:44 +0530 Subject: [PATCH 018/114] feat: Warehouse Capacity Summary - Added Page Warehouse Capacity Summary - Added Page to Desk and Putaway List View - Reused Item Dashboard/Stock Balance page render code - Added naming series to Putaway Rule --- erpnext/public/build.json | 4 +- erpnext/stock/dashboard/item_dashboard.js | 75 ++++++++--- .../dashboard/warehouse_capacity_dashboard.py | 69 ++++++++++ erpnext/stock/desk_page/stock/stock.json | 4 +- erpnext/stock/doctype/item/item.js | 5 +- .../purchase_receipt/purchase_receipt.json | 9 +- .../doctype/putaway_rule/putaway_rule.json | 8 +- .../doctype/putaway_rule/putaway_rule_list.js | 10 +- .../stock/page/stock_balance/stock_balance.js | 3 + .../warehouse_capacity_summary/__init__.py | 0 .../warehouse_capacity_summary.html | 40 ++++++ .../warehouse_capacity_summary.js | 120 ++++++++++++++++++ .../warehouse_capacity_summary.json | 26 ++++ .../warehouse_capacity_summary_header.html | 19 +++ 14 files changed, 367 insertions(+), 25 deletions(-) create mode 100644 erpnext/stock/dashboard/warehouse_capacity_dashboard.py create mode 100644 erpnext/stock/page/warehouse_capacity_summary/__init__.py create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2695502269..8b18a1fcfb 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -54,6 +54,8 @@ "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard_list.html", - "stock/dashboard/item_dashboard.js" + "stock/dashboard/item_dashboard.js", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html" ] } diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 9bd03d45cb..abc286fcc6 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -24,6 +24,16 @@ erpnext.stock.ItemDashboard = Class.extend({ handle_move_add($(this), "Add") }); + this.content.on('click', '.btn-edit', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + let company = unescape($(this).attr('data-company')); + frappe.db.get_value('Putaway Rule', + {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { + frappe.set_route("Form", "Putaway Rule", r.name); + }); + }); + function handle_move_add(element, action) { let item = unescape(element.attr('data-item')); let warehouse = unescape(element.attr('data-warehouse')); @@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += 20; + me.start += this.page_length; me.refresh(); }); @@ -69,33 +79,41 @@ erpnext.stock.ItemDashboard = Class.extend({ this.before_refresh(); } + let args = { + item_code: this.item_code, + warehouse: this.warehouse, + parent_warehouse: this.parent_warehouse, + item_group: this.item_group, + company: this.company, + start: this.start, + sort_by: this.sort_by, + sort_order: this.sort_order + } + var me = this; frappe.call({ - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - args: { - item_code: this.item_code, - warehouse: this.warehouse, - item_group: this.item_group, - start: this.start, - sort_by: this.sort_by, - sort_order: this.sort_order, - }, + method: this.method, + args: args, callback: function(r) { me.render(r.message); } }); }, render: function(data) { - if(this.start===0) { + if (this.start===0) { this.max_count = 0; this.result.empty(); } + if (this.page_name === "warehouse-capacity-summary") { + var context = this.get_capacity_dashboard_data(data); + } else { + var context = this.get_item_dashboard_data(data, this.max_count, true); + } - var context = this.get_item_dashboard_data(data, this.max_count, true); this.max_count = this.max_count; // show more button - if(data && data.length===21) { + if (data && data.length===(this.page_length + 1)) { this.content.find('.more').removeClass('hidden'); // remove the last element @@ -106,12 +124,17 @@ erpnext.stock.ItemDashboard = Class.extend({ // If not any stock in any warehouses provide a message to end user if (context.data.length > 0) { - $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result); + this.content.find('.result').css('text-align', 'unset'); + $(frappe.render_template(this.template, context)).appendTo(this.result); } else { - var message = __("Currently no stock available in any warehouse"); - $(` ${message} `).appendTo(this.result); + var message = __("No Stock Available Currently"); + this.content.find('.result').css('text-align', 'center'); + + $(`
    + ${message}
    `).appendTo(this.result); } }, + get_item_dashboard_data: function(data, max_count, show_item) { if(!max_count) max_count = 0; if(!data) data = []; @@ -128,7 +151,7 @@ erpnext.stock.ItemDashboard = Class.extend({ d.total_reserved, max_count); }); - var can_write = 0; + let can_write = 0; if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ can_write = 1; } @@ -139,6 +162,24 @@ erpnext.stock.ItemDashboard = Class.extend({ can_write:can_write, show_item: show_item || false } + }, + + get_capacity_dashboard_data: function(data) { + if(!data) data = []; + + data.forEach(function(d) { + d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; + }); + + let can_write = 0; + if(frappe.boot.user.can_write.indexOf("Putaway Rule")>=0){ + can_write = 1; + } + + return { + data: data, + can_write: can_write, + } } }) diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py new file mode 100644 index 0000000000..ab573e566a --- /dev/null +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import frappe +from frappe.model.db_query import DatabaseQuery +from frappe.utils import nowdate +from frappe.utils import flt +from erpnext.stock.utils import get_stock_balance + +@frappe.whitelist() +def get_data(item_code=None, warehouse=None, parent_warehouse=None, + company=None, start=0, sort_by="stock_capacity", sort_order="desc"): + """Return data to render the warehouse capacity dashboard.""" + filters = get_filters(item_code, warehouse, parent_warehouse, company) + + no_permission, filters = get_warehouse_filter_based_on_permissions(filters) + if no_permission: + return [] + + capacity_data = get_warehouse_capacity_data(filters, start) + + asc_desc = -1 if sort_order == "desc" else 1 + capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + + return capacity_data + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, + company=None): + filters = [['disable', '=', 0]] + if item_code: + filters.append(['item_code', '=', item_code]) + if warehouse: + filters.append(['warehouse', '=', warehouse]) + if company: + filters.append(['company', '=', company]) + if parent_warehouse: + lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) + warehouses = frappe.db.sql_list(""" + select name from `tabWarehouse` + where lft >=%s and rgt<=%s + """, (lft, rgt)) + filters.append(['warehouse', 'in', warehouses]) + return filters + +def get_warehouse_filter_based_on_permissions(filters): + try: + # check if user has any restrictions based on user permissions on warehouse + if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): + filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + return False, filters + except frappe.PermissionError: + # user does not have access on warehouse + return True, [] + +def get_warehouse_capacity_data(filters, start): + capacity_data = frappe.db.get_all('Putaway Rule', + fields=['item_code', 'warehouse','stock_capacity', 'company'], + filters=filters, + limit_start=start, + limit_page_length='11' + ) + + for entry in capacity_data: + balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 + entry.update({ + 'actual_qty': balance_qty, + 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) + }) + + return capacity_data \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index aa4fc28ec9..0038c0a971 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Stock Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Putaway Rule\"\n ],\n \"label\": \"Warehouse Capacity Summary\",\n \"name\": \"warehouse-capacity-summary\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-11-24 15:43:20.496057", + "modified": "2020-11-26 10:43:48.286663", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index faeeb578fe..ec32b0f044 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -384,7 +384,10 @@ $.extend(erpnext.item, { ' + __("Stock Levels") + ''); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, - item_code: frm.doc.name + item_code: frm.doc.name, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }); erpnext.item.item_dashboard.refresh(); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 13c8ceb759..7213eb8616 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,6 +21,7 @@ "posting_date", "posting_time", "set_posting_time", + "apply_putaway_rule", "is_return", "return_against", "section_addresses", @@ -1104,13 +1105,19 @@ "fieldtype": "Small Text", "label": "Billing Address", "read_only": 1 + }, + { + "default": "0", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-10-30 14:00:08.347534", + "modified": "2020-11-25 18:31:32.234503", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 325e6f1355..a003f4986f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:{item_code}-{warehouse}", + "autoname": "PUT-.####", "creation": "2020-11-09 11:39:46.489501", "doctype": "DocType", "editable_grid": 1, @@ -90,12 +90,14 @@ "fieldname": "uom", "fieldtype": "Link", "label": "UOM", + "no_copy": 1, "options": "UOM" }, { "fieldname": "stock_capacity", "fieldtype": "Float", "label": "Capacity in Stock UOM", + "no_copy": 1, "read_only": 1 }, { @@ -103,12 +105,13 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "Conversion Factor", + "no_copy": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-24 16:20:18.306671", + "modified": "2020-11-25 20:39:19.973437", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", @@ -152,5 +155,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "item_code", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index e48c415f14..725e91ee8d 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -6,5 +6,13 @@ frappe.listview_settings['Putaway Rule'] = { } else { return [__("Active"), "blue", "disable,=,0"]; } - } + }, + + reports: [ + { + name: 'Warehouse Capacity Summary', + report_type: 'Page', + route: 'warehouse-capacity-summary' + } + ] }; diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index da21c6bc64..bddffd465e 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -65,6 +65,9 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { frappe.require('assets/js/item-dashboard.min.js', function() { page.item_dashboard = new erpnext.stock.ItemDashboard({ parent: page.main, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }) page.item_dashboard.before_refresh = function() { diff --git a/erpnext/stock/page/warehouse_capacity_summary/__init__.py b/erpnext/stock/page/warehouse_capacity_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html new file mode 100644 index 0000000000..90112c78a8 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -0,0 +1,40 @@ +{% for d in data %} +
    +
    + + +
    + {{ d.stock_capacity }} +
    +
    + {{ d.actual_qty }} +
    +
    +
    +
    +
    +
    +
    +
    + {{ d.percent_occupied }}% +
    + {% if can_write %} +
    +
    + {% endif %} +
    +
    +{% endfor %} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js new file mode 100644 index 0000000000..c3b3b5d8ec --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -0,0 +1,120 @@ +frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Warehouse Capacity Summary', + single_column: true + }); + page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync'); + page.start = 0; + + page.company_field = page.add_field({ + fieldname: 'company', + label: __('Company'), + fieldtype:'Link', + options:'Company', + reqd: 1, + default: frappe.defaults.get_default("company"), + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.warehouse_field = page.add_field({ + fieldname: 'warehouse', + label: __('Warehouse'), + fieldtype:'Link', + options:'Warehouse', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.item_field = page.add_field({ + fieldname: 'item_code', + label: __('Item'), + fieldtype:'Link', + options:'Item', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.parent_warehouse_field = page.add_field({ + fieldname: 'parent_warehouse', + label: __('Parent Warehouse'), + fieldtype:'Link', + options:'Warehouse', + get_query: function() { + return { + filters: { + "is_group": 1 + } + }; + }, + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.sort_selector = new frappe.ui.SortSelector({ + parent: page.wrapper.find('.page-form'), + args: { + sort_by: 'stock_capacity', + sort_order: 'desc', + options: [ + {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, + {fieldname: 'percent_occupied', label:__('% Occupied')}, + {fieldname: 'actual_qty', label:__('Balance Qty (Stock ')} + ] + }, + change: function(sort_by, sort_order) { + page.capacity_dashboard.sort_by = sort_by; + page.capacity_dashboard.sort_order = sort_order; + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + frappe.require('assets/js/item-dashboard.min.js', function() { + $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main); + + page.capacity_dashboard = new erpnext.stock.ItemDashboard({ + page_name: "warehouse-capacity-summary", + page_length: 10, + parent: page.main, + sort_by: 'stock_capacity', + sort_order: 'desc', + method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', + template: 'warehouse_capacity_summary' + }) + + page.capacity_dashboard.before_refresh = function() { + this.item_code = page.item_field.get_value(); + this.warehouse = page.warehouse_field.get_value(); + this.parent_warehouse = page.parent_warehouse_field.get_value(); + this.company = page.company_field.get_value(); + } + + page.capacity_dashboard.refresh(); + + let setup_click = function(doctype) { + page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { + var name = $(this).attr('data-name'); + var field = page[doctype.toLowerCase() + '_field']; + if(field.get_value()===name) { + frappe.set_route('Form', doctype, name) + } else { + field.set_input(name); + page.capacity_dashboard.refresh(); + } + }); + } + + setup_click('Item'); + setup_click('Warehouse'); + }); +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json new file mode 100644 index 0000000000..a6e5b45332 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2020-11-25 12:07:54.056208", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-11-25 11:07:54.056208", + "modified_by": "Administrator", + "module": "Stock", + "name": "warehouse-capacity-summary", + "owner": "Administrator", + "page_name": "Warehouse Capacity Summary", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Warehouse Capacity Summary" +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html new file mode 100644 index 0000000000..acaf180a90 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -0,0 +1,19 @@ +
    +
    +
    + Warehouse +
    +
    + Item +
    +
    + Stock Capacity +
    +
    + Balance Stock Qty +
    +
    + % Occupied +
    +
    +
    \ No newline at end of file From 2c114053ad3087ad12a8d8a084cabb817b126066 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 Nov 2020 15:05:28 +0530 Subject: [PATCH 019/114] feat: Patient History Settings --- .../__init__.py | 0 .../patient_history_custom_document_type.json | 46 ++++++++++ .../patient_history_custom_document_type.py | 10 +++ .../patient_history_settings/__init__.py | 0 .../patient_history_settings.js | 85 +++++++++++++++++++ .../patient_history_settings.json | 55 ++++++++++++ .../patient_history_settings.py | 10 +++ .../test_patient_history_settings.py | 10 +++ .../__init__.py | 0 ...atient_history_standard_document_type.json | 47 ++++++++++ .../patient_history_standard_document_type.py | 10 +++ 11 files changed, 273 insertions(+) create mode 100644 erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json create mode 100644 erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py create mode 100644 erpnext/healthcare/doctype/patient_history_settings/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js create mode 100644 erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json create mode 100644 erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py create mode 100644 erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py create mode 100644 erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json create mode 100644 erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py b/erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json new file mode 100644 index 0000000000..a158075e7b --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2020-11-25 13:40:23.054469", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "select_fields", + "selected_fields" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "select_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Select Fields" + }, + { + "fieldname": "selected_fields", + "fieldtype": "Code", + "label": "selected_fields" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-25 14:19:33.637543", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient History Custom Document Type", + "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_history_custom_document_type/patient_history_custom_document_type.py b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py new file mode 100644 index 0000000000..f0a1f929f4 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.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 PatientHistoryCustomDocumentType(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_history_settings/__init__.py b/erpnext/healthcare/doctype/patient_history_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js new file mode 100644 index 0000000000..155476e2b1 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -0,0 +1,85 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Patient History Settings', { + refresh: function(frm) { + frm.set_query('document_type', 'custom_doctypes', () => { + return { + filters: { + custom: 1, + module: 'Healthcare' + } + }; + }); + }, + + field_selector: function(frm, doc) { + let document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + let d = new frappe.ui.Dialog({ + title: __('{0} Fields', [__(doc.document_type)]), + fields: [ + { + label: __('Select Fields'), + fieldtype: 'MultiCheck', + fieldname: 'fields', + options: frm.events.get_doctype_fields(frm, doc.document_type, document_fields), + columns: 2 + } + ] + }); + + d.set_primary_action(__('Save'), () => { + let values = d.get_values().fields; + + let selected_fields = []; + + for (let idx in values) { + let value = values[idx]; + + let field = frappe.meta.get_docfield(doc.document_type, value); + if (field) { + selected_fields.push({ + label: field.label, + fieldname: field.fieldname + }); + } + } + + frappe.model.set_value('Patient History Custom Document Type', doc.name, 'selected_fields', JSON.stringify(selected_fields)); + d.hide(); + }); + + d.show(); + }, + + get_doctype_fields(frm, document_type, fields) { + let multiselect_fields = []; + + frappe.model.with_doctype(document_type, () => { + // get doctype fields + frappe.get_doc('DocType', document_type).fields.forEach(field => { + if (!in_list(frappe.model.no_value_type, field.fieldtype) && !field.hidden) { + multiselect_fields.push({ + label: field.label, + value: field.fieldname, + checked: in_list(fields, field.fieldname) + }); + } + }); + }); + + return multiselect_fields; + } +}); + +frappe.ui.form.on('Patient History Custom Document Type', { + select_fields: function(frm) { + let doc = frm.selected_doc; + + if (!doc.document_type) + frappe.throw(__('Select the Document Type first.')) + + frm.events.field_selector(frm, doc); + } + +}); diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json new file mode 100644 index 0000000000..143e2c91eb --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2020-11-25 13:41:37.675518", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "standard_doctypes", + "section_break_2", + "custom_doctypes" + ], + "fields": [ + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "custom_doctypes", + "fieldtype": "Table", + "label": "Custom Document Types", + "options": "Patient History Custom Document Type" + }, + { + "fieldname": "standard_doctypes", + "fieldtype": "Table", + "label": "Standard Document Types", + "options": "Patient History Standard Document Type", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-11-25 13:43:38.511771", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient History 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/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py new file mode 100644 index 0000000000..27cbf2fc60 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.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 PatientHistorySettings(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py new file mode 100644 index 0000000000..548c423670 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_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 TestPatientHistorySettings(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py b/erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json new file mode 100644 index 0000000000..ec40d893eb --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json @@ -0,0 +1,47 @@ +{ + "actions": [], + "creation": "2020-11-25 13:39:36.014814", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "select_fields", + "selected_fields" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "select_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Select Fields" + }, + { + "fieldname": "selected_fields", + "fieldtype": "Code", + "label": "Selected Fields" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-25 14:19:53.708991", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient History Standard Document Type", + "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_history_standard_document_type/patient_history_standard_document_type.py b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py new file mode 100644 index 0000000000..2d94911855 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.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 PatientHistoryStandardDocumentType(Document): + pass From f2932d720882433e18c690be130a7d919a0570d6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 28 Nov 2020 19:19:58 +0530 Subject: [PATCH 020/114] feat: set date field in Settings for Patient Medical Record --- .../patient_history_custom_document_type.json | 24 ++++++++++++------- .../patient_history_settings.js | 13 ++++------ .../patient_history_settings.py | 17 +++++++++++-- ...atient_history_standard_document_type.json | 16 ++++++------- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json index a158075e7b..7986e48ced 100644 --- a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "document_type", - "select_fields", + "date_fieldname", + "add_edit_fields", "selected_fields" ], "fields": [ @@ -18,22 +19,29 @@ "options": "DocType", "reqd": 1 }, - { - "fieldname": "select_fields", - "fieldtype": "Button", - "in_list_view": 1, - "label": "Select Fields" - }, { "fieldname": "selected_fields", "fieldtype": "Code", "label": "selected_fields" + }, + { + "fieldname": "add_edit_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Add / Edit Fields" + }, + { + "fieldname": "date_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Date Fieldname", + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-25 14:19:33.637543", + "modified": "2020-11-28 19:04:48.323164", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Custom Document Type", diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 155476e2b1..ca2707f6a6 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -73,13 +73,10 @@ frappe.ui.form.on('Patient History Settings', { }); frappe.ui.form.on('Patient History Custom Document Type', { - select_fields: function(frm) { - let doc = frm.selected_doc; - - if (!doc.document_type) - frappe.throw(__('Select the Document Type first.')) - - frm.events.field_selector(frm, doc); + add_edit_fields: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.document_type) { + frm.events.field_selector(frm, row); + } } - }); diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 27cbf2fc60..9e876e8c95 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -3,8 +3,21 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ from frappe.model.document import Document class PatientHistorySettings(Document): - pass + def validate(self): + self.validate_date_fieldnames() + + def validate_date_fieldnames(self): + for entry in self.custom_doctypes: + field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname) + if not field: + frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format( + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + + if field.fieldtype not in ['Date', 'Datetime']: + frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format( + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json index ec40d893eb..ef4fc2bfe1 100644 --- a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json @@ -6,7 +6,7 @@ "engine": "InnoDB", "field_order": [ "document_type", - "select_fields", + "add_edit_fields", "selected_fields" ], "fields": [ @@ -19,22 +19,22 @@ "read_only": 1, "reqd": 1 }, - { - "fieldname": "select_fields", - "fieldtype": "Button", - "in_list_view": 1, - "label": "Select Fields" - }, { "fieldname": "selected_fields", "fieldtype": "Code", "label": "Selected Fields" + }, + { + "fieldname": "add_edit_fields", + "fieldtype": "Button", + "in_list_view": 1, + "label": "Add / Edit Fields" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-25 14:19:53.708991", + "modified": "2020-11-28 18:57:30.446348", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Standard Document Type", From c91e03c8911286cb50731dd7064501b9c80474a9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 28 Nov 2020 20:24:06 +0530 Subject: [PATCH 021/114] feat: Create Patient Medical Record on configured doctype submission --- .../patient_history_settings.js | 6 ++- .../patient_history_settings.py | 53 ++++++++++++++++++- erpnext/hooks.py | 4 ++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index ca2707f6a6..60926eeb11 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -40,7 +40,8 @@ frappe.ui.form.on('Patient History Settings', { if (field) { selected_fields.push({ label: field.label, - fieldname: field.fieldname + fieldname: field.fieldname, + fieldtype: field.fieldtype }); } } @@ -58,7 +59,8 @@ frappe.ui.form.on('Patient History Settings', { frappe.model.with_doctype(document_type, () => { // get doctype fields frappe.get_doc('DocType', document_type).fields.forEach(field => { - if (!in_list(frappe.model.no_value_type, field.fieldtype) && !field.hidden) { + if (!in_list(frappe.model.no_value_type, field.fieldtype) || + in_list(frappe.model.table_fields, field.fieldtype) && !field.hidden) { multiselect_fields.push({ label: field.label, value: field.fieldname, diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 9e876e8c95..af8c6f4557 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import cstr from frappe.model.document import Document class PatientHistorySettings(Document): @@ -20,4 +21,54 @@ class PatientHistorySettings(Document): if field.fieldtype not in ['Date', 'Datetime']: frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format( - entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) \ No newline at end of file + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + + +def create_medical_record(doc, method=None): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ + frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + return + + subject = set_subject_field(doc) + date_field = get_date_field(doc.doctype) + medical_record = frappe.new_doc('Patient Medical Record') + medical_record.patient = doc.patient + medical_record.subject = subject + medical_record.status = 'Open' + medical_record.communication_date = doc.get(date_field) + medical_record.reference_doctype = doc.doctype + medical_record.reference_name = doc.name + medical_record.reference_owner = doc.owner + medical_record.save(ignore_permissions=True) + + +def set_subject_field(doc): + from frappe.utils.formatters import format_value + + meta = frappe.get_meta(doc.doctype) + subject = '' + patient_history_fields = get_patient_history_fields(doc) + + for entry in patient_history_fields: + fieldname = entry.get('fieldname') + if doc.get(fieldname): + formated_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) + subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formated_value) + subject += '
    ' + + return subject + + +def get_date_field(doctype): + return frappe.db.get_value('Patient History Custom Document Type', + { 'document_type': doctype }, 'date_fieldname') + + +def get_patient_history_fields(doc): + import json + patient_history_fields = frappe.db.get_value('Patient History Custom Document Type', + { 'document_type': doc.doctype }, 'selected_fields') + + if patient_history_fields: + return json.loads(patient_history_fields) + diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 741176f33f..4ee42c7559 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -221,6 +221,10 @@ standard_queries = { } doc_events = { + "*": { + "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", + "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" + }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty" From a75f79762f224f8b38d2877b7a596cc93d9647fd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 28 Nov 2020 23:09:07 +0530 Subject: [PATCH 022/114] feat: Link for individual documents in Patient History --- .../page/patient_history/patient_history.html | 1 - .../page/patient_history/patient_history.js | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index 7a9446dffd..60f4501fed 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -1,6 +1,5 @@
    -

    {%= __("Select Patient") %}

    diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index fe5b7bc488..3e6d790ca7 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -16,6 +16,8 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { fieldtype: "Link", options: "Patient", fieldname: "patient", + placeholder: __('Select Patient'), + only_select: true, change: function(){ if(pid != patient.get_value() && patient.get_value()){ me.start = 0; @@ -27,7 +29,6 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { pid = patient.get_value(); } }, - only_input: true, }); patient.refresh(); @@ -120,7 +121,11 @@ var add_to_records = function(me, data){ data[i].imgsrc = false; } var time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``; - time_line_heading += data[i].reference_doctype + " - "+ data[i].reference_name; + time_line_heading += data[i].reference_doctype + " - " + + ` + ${data[i].reference_name} + ` + details += `
  • `; @@ -135,7 +140,7 @@ var add_to_records = function(me, data){ } details += `
    - `+time_line_heading+` on + `+time_line_heading+` ${data[i].date_sep} @@ -172,11 +177,11 @@ var add_date_separator = function(data) { var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); if(diff < 1) { - var pdate = 'Today'; + var pdate = __('Today'); } else if(diff < 2) { - pdate = 'Yesterday'; + pdate = __('Yesterday'); } else { - pdate = frappe.datetime.global_date_format(date); + pdate = __('on ') + frappe.datetime.global_date_format(date); } data.date_sep = pdate; return data; @@ -227,7 +232,7 @@ var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { }, callback: function(r) { if (r.message){ - var show_chart_btns_html = "
    "); - me.page.main.find("."+docname).parent().find('.document-html').show(); - me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched', "1"); + if (r.message) { + me.page.main.find('.' + docname).hide(); + + me.page.main.find('.' + docname).parent().find('.document-html').html( + `${r.message.html} +
    + + +
    + `); + + me.page.main.find('.' + docname).parent().find('.document-html').show(); + me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); } - }, - freeze: true + } }); } } }); - this.page.main.on("click", ".btn-less", function() { - var docname = $(this).attr("data-docname"); - me.page.main.find("."+docname).parent().find('.document-id').show(); - me.page.main.find("."+docname).parent().find('.document-html').hide(); + this.page.main.on('click', '.btn-less', function() { + let docname = $(this).attr('data-docname'); + me.page.main.find('.' + docname).parent().find('.document-id').show(); + me.page.main.find('.' + docname).parent().find('.document-html').hide(); }); me.start = 0; - me.page.main.on("click", ".btn-get-records", function(){ + me.page.main.on('click', '.btn-get-records', function(){ get_documents(patient.get_value(), me); }); }; -var get_documents = function(patient, me){ +let get_documents = function(patient, me) { frappe.call({ - "method": "erpnext.healthcare.page.patient_history.patient_history.get_feed", + 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', args: { name: patient, start: me.start, page_length: 20 }, - callback: function (r) { - var data = r.message; - if(data.length){ + callback: function(r) { + let data = r.message; + if (data.length) { add_to_records(me, data); - }else{ - me.page.main.find(".patient_documents_list").append("


    No more records..

    "); - me.page.main.find(".btn-get-records").hide(); + } else { + me.page.main.find('.patient_documents_list').append(` +
    +

    ${__('No more records..')}

    +
    `); + me.page.main.find('.btn-get-records').hide(); } } }); }; -var add_to_records = function(me, data){ - var details = ""; - me.page.main.find(".patient_documents_list").append(details); + + details += '
'; + me.page.main.find('.patient_documents_list').append(details); me.start += data.length; - if(data.length===20){ + + if (data.length === 20) { me.page.main.find(".btn-get-records").show(); - }else{ + } else { me.page.main.find(".btn-get-records").hide(); - me.page.main.find(".patient_documents_list").append("


No more records..

"); + me.page.main.find(".patient_documents_list").append(` +
+

${__('No more records..')}

+
`); } }; -var add_date_separator = function(data) { - var date = frappe.datetime.str_to_obj(data.creation); +let add_date_separator = function(data) { + let date = frappe.datetime.str_to_obj(data.creation); + let pdate = ''; + let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - if(diff < 1) { - var pdate = __('Today'); - } else if(diff < 2) { + if (diff < 1) { + pdate = __('Today'); + } else if (diff < 2) { pdate = __('Yesterday'); } else { pdate = __('on ') + frappe.datetime.global_date_format(date); @@ -187,107 +212,118 @@ var add_date_separator = function(data) { return data; }; -var show_patient_info = function(patient, me){ +let show_patient_info = function(patient, me) { frappe.call({ - "method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', args: { patient: patient }, - callback: function (r) { - var data = r.message; - var details = ""; - if(data.image){ - details += "
"; + callback: function(r) { + let data = r.message; + let details = ''; + if (data.image){ + details += `
`; } - details += "" + data.patient_name +"
" + data.sex; - if(data.email) details += "
" + data.email; - if(data.mobile) details += "
" + data.mobile; - if(data.occupation) details += "

Occupation : " + data.occupation; - if(data.blood_group) details += "
Blood group : " + data.blood_group; - if(data.allergies) details += "

Allergies : "+ data.allergies.replace("\n", "
"); - if(data.medication) details += "
Medication : "+ data.medication.replace("\n", "
"); - if(data.alcohol_current_use) details += "

Alcohol use : "+ data.alcohol_current_use; - if(data.alcohol_past_use) details += "
Alcohol past use : "+ data.alcohol_past_use; - if(data.tobacco_current_use) details += "
Tobacco use : "+ data.tobacco_current_use; - if(data.tobacco_past_use) details += "
Tobacco past use : "+ data.tobacco_past_use; - if(data.medical_history) details += "

Medical history : "+ data.medical_history.replace("\n", "
"); - if(data.surgical_history) details += "
Surgical history : "+ data.surgical_history.replace("\n", "
"); - if(data.surrounding_factors) details += "

Occupational hazards : "+ data.surrounding_factors.replace("\n", "
"); - if(data.other_risk_factors) details += "
Other risk factors : " + data.other_risk_factors.replace("\n", "
"); - if(data.patient_details) details += "

More info : " + data.patient_details.replace("\n", "
"); - if(details){ - details = "
" + details + "
"; + details += ` ${data.patient_name}
${data.sex}`; + if (data.email) details += `
${data.email}` + if (data.mobile) details += `
${data.mobile}`; + if (data.occupation) details += `

${__('Occupation')} : ${data.occupation}`; + if (data.blood_group) details += `
${__('Blood Group')} : ${data.blood_group}`; + if (data.allergies) details += `

${__('Allerigies')} : ${data.allergies.replace("\n", ", ")}`; + if (data.medication) details += `
${__('Medication')} : ${data.medication.replace("\n", ", ")}`; + if (data.alcohol_current_use) details += `

${__('Alcohol use')} : ${data.alcohol_current_use}`; + if (data.alcohol_past_use) details += `
${__('Alcohol past use')} : ${data.alcohol_past_use}`; + if (data.tobacco_current_use) details += `
${__('Tobacco use')} : ${data.tobacco_current_use}`; + if (data.tobacco_past_use) details += `
${__('Tobacco past use')} : ${data.tobacco_past_use}`; + if (data.medical_history) details += `

${__('Medical history')} : ${data.medical_history.replace("\n", ", ")}`; + if (data.surgical_history) details += `
${__('Surgical history')} : ${data.surgical_history.replace("\n", ", ")}`; + if (data.surrounding_factors) details += `

${__('Occupational hazards')} : ${data.surrounding_factors.replace("\n", ", ")}`; + if (data.other_risk_factors) details += `
${__('Other risk factors')} : ${data.other_risk_factors.replace("\n", ", ")}`; + if (data.patient_details) details += `

${__('More info')} : ${data.patient_details.replace("\n", ", ")}`; + + if (details){ + details = `
` + details + `
`; } - me.page.main.find(".patient_details").html(details); + me.page.main.find('.patient_details').html(details); } }); }; -var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { +let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { frappe.call({ - method: "erpnext.healthcare.utils.get_patient_vitals", + method: 'erpnext.healthcare.utils.get_patient_vitals', args:{ patient: patient }, callback: function(r) { - if (r.message){ - var show_chart_btns_html = ""; - me.page.main.find(".show_chart_btns").html(show_chart_btns_html); - var data = r.message; + if (r.message) { + let show_chart_btns_html = ` + `; + + me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + let data = r.message; let labels = [], datasets = []; let bp_systolic = [], bp_diastolic = [], temperature = []; let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; - for(var i=0; i d + ' ' + pts, } }); - }else{ - me.page.main.find(".patient_vital_charts").html(""); - me.page.main.find(".show_chart_btns").html(""); + } else { + me.page.main.find('.patient_vital_charts').html(''); + me.page.main.find('.show_chart_btns').html(''); } } }); From fc1e352adf3f7efbbf243a029714d3c8eef0720f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 29 Nov 2020 22:38:14 +0530 Subject: [PATCH 024/114] feat: Doctype filter for Patient History --- .../page/patient_history/patient_history.css | 5 ++ .../page/patient_history/patient_history.html | 8 +++ .../page/patient_history/patient_history.js | 58 ++++++++++++++---- .../page/patient_history/patient_history.py | 59 ++++++++++++------- 4 files changed, 98 insertions(+), 32 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css index 865d6abee0..1bb589164e 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.css +++ b/erpnext/healthcare/page/patient_history/patient_history.css @@ -109,6 +109,11 @@ padding-right: 0px; } +.patient-history-filter { + margin-left: 35px; + width: 25%; +} + #page-medical_record .plot-wrapper { padding: 20px 15px; border-bottom: 1px solid #d1d8dd; diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index 60f4501fed..00b38e7258 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -10,6 +10,14 @@
+
+
+
+
+
+
+
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 5f1851fb0f..40b86fdff4 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -20,14 +20,16 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { placeholder: __('Select Patient'), only_select: true, change: function(){ - if (pid != patient.get_value() && patient.get_value()) { + let patient_id = patient.get_value(); + if (pid != patient_id && patient_id) { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - get_documents(patient.get_value(), me); - show_patient_info(patient.get_value(), me); - show_patient_vital_charts(patient.get_value(), me, 'bp', 'mmHg', 'Blood Pressure'); + setup_filters(patient_id, me) + get_documents(patient_id, me); + show_patient_info(patient_id, me); + show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure'); } - pid = patient.get_value(); + pid = patient_id; } }, }); @@ -93,14 +95,48 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { }); }; -let get_documents = function(patient, me) { +let setup_filters = function(patient, me) { + frappe.xcall( + 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes' + ).then(document_types => { + let doctype_filter = frappe.ui.form.make_control({ + parent: $('.doctype-filter'), + df: { + fieldtype: 'MultiSelectList', + fieldname: 'document_type', + placeholder: __('Select Document Type'), + input_class: 'input-xs', + change: () => { + me.start = 0; + me.page.main.find('.patient_documents_list').html(''); + get_documents(patient, me, doctype_filter.get_value()); + }, + get_data: () => { + return document_types.map(document_type => { + return { + description: document_type, + value: document_type + }; + }); + }, + } + }); + doctype_filter.refresh(); + }) +} + +let get_documents = function(patient, me, document_types="") { + let filters = { + name: patient, + start: me.start, + page_length: 20 + }; + if (document_types) + filters['document_types'] = document_types; + frappe.call({ 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', - args: { - name: patient, - start: me.start, - page_length: 20 - }, + args: filters, callback: function(r) { let data = r.message; if (data.length) { diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index 772aa4ef5e..c04b376197 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -4,36 +4,53 @@ from __future__ import unicode_literals import frappe +import json from frappe.utils import cint from erpnext.healthcare.utils import render_docs_as_html @frappe.whitelist() -def get_feed(name, start=0, page_length=20): +def get_feed(name, document_types=None, start=0, page_length=20): """get feed""" - result = frappe.db.sql("""select name, owner, creation, - reference_doctype, reference_name, subject - from `tabPatient Medical Record` - where patient=%(patient)s - order by creation desc - limit %(start)s, %(page_length)s""", - { - "patient": name, - "start": cint(start), - "page_length": cint(page_length) - }, as_dict=True) + filters = {'patient': name} + if document_types: + document_types = json.loads(document_types) + filters['reference_doctype'] = ['IN', document_types] + + result = frappe.db.get_all('Patient Medical Record', + fields=['name', 'owner', 'creation', + 'reference_doctype', 'reference_name', 'subject'], + filters=filters, + order_by='creation DESC', + limit=cint(page_length), + start=cint(start) + ) + return result @frappe.whitelist() def get_feed_for_dt(doctype, docname): """get feed""" - result = frappe.db.sql("""select name, owner, modified, creation, - reference_doctype, reference_name, subject - from `tabPatient Medical Record` - where reference_name=%(docname)s and reference_doctype=%(doctype)s - order by creation desc""", - { - "docname": docname, - "doctype": doctype - }, as_dict=True) + result = frappe.db.get_all('Patient Medical Record', + fields=['name', 'owner', 'creation', + 'reference_doctype', 'reference_name', 'subject'], + filters={ + 'reference_doctype': doctype, + 'reference_name': docname + }, + order_by='creation DESC' + ) return result + +@frappe.whitelist() +def get_patient_history_doctypes(): + document_types = [] + settings = frappe.get_single("Patient History Settings") + + for entry in settings.standard_doctypes: + document_types.append(entry.document_type) + + for entry in settings.custom_doctypes: + document_types.append(entry.document_type) + + return document_types \ No newline at end of file From 4af3d4e4bb745f35549ce3b638981206c1f84a3e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 29 Nov 2020 22:43:56 +0530 Subject: [PATCH 025/114] fix: feed not visible when filter is reset --- erpnext/healthcare/page/patient_history/patient_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index c04b376197..b8494c9e58 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -14,7 +14,8 @@ def get_feed(name, document_types=None, start=0, page_length=20): filters = {'patient': name} if document_types: document_types = json.loads(document_types) - filters['reference_doctype'] = ['IN', document_types] + if len(document_types): + filters['reference_doctype'] = ['IN', document_types] result = frappe.db.get_all('Patient Medical Record', fields=['name', 'owner', 'creation', From 4d6d115a4d43aa3b7c6ebf2604998a2a4728050c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 29 Nov 2020 23:16:12 +0530 Subject: [PATCH 026/114] feat: date range filter for Patient History --- .../page/patient_history/patient_history.html | 3 +- .../page/patient_history/patient_history.js | 27 +++++++++++++++-- .../page/patient_history/patient_history.py | 30 ++++++++++++++----- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index 00b38e7258..deaaa97868 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -14,8 +14,7 @@
-
-
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 40b86fdff4..5a6295b707 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -109,7 +109,7 @@ let setup_filters = function(patient, me) { change: () => { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value()); + get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value()); }, get_data: () => { return document_types.map(document_type => { @@ -122,10 +122,29 @@ let setup_filters = function(patient, me) { } }); doctype_filter.refresh(); - }) + + let date_range_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'DateRange', + fieldname: 'date_range', + placeholder: __('Date Range'), + input_class: 'input-xs', + change: () => { + let selected_date_range = date_range_field.get_value(); + if (selected_date_range && selected_date_range.length === 2) { + me.start = 0; + me.page.main.find('.patient_documents_list').html(''); + get_documents(patient, me, doctype_filter.get_value(), selected_date_range); + } + } + }, + parent: $('.date-filter') + }); + date_range_field.refresh(); + }); } -let get_documents = function(patient, me, document_types="") { +let get_documents = function(patient, me, document_types="", selected_date_range="") { let filters = { name: patient, start: me.start, @@ -133,6 +152,8 @@ let get_documents = function(patient, me, document_types="") { }; if (document_types) filters['document_types'] = document_types; + if (selected_date_range) + filters['date_range'] = selected_date_range; frappe.call({ 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index b8494c9e58..de8a5771d2 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -9,13 +9,9 @@ from frappe.utils import cint from erpnext.healthcare.utils import render_docs_as_html @frappe.whitelist() -def get_feed(name, document_types=None, start=0, page_length=20): +def get_feed(name, document_types=None, date_range=None, start=0, page_length=20): """get feed""" - filters = {'patient': name} - if document_types: - document_types = json.loads(document_types) - if len(document_types): - filters['reference_doctype'] = ['IN', document_types] + filters = get_filters(name, document_types, date_range) result = frappe.db.get_all('Patient Medical Record', fields=['name', 'owner', 'creation', @@ -28,6 +24,25 @@ def get_feed(name, document_types=None, start=0, page_length=20): return result + +def get_filters(name, document_types=None, date_range=None): + filters = {'patient': name} + if document_types: + document_types = json.loads(document_types) + if len(document_types): + filters['reference_doctype'] = ['IN', document_types] + + if date_range: + try: + date_range = json.loads(date_range) + if date_range: + filters['creation'] = ['between', [date_range[0], date_range[1]]] + except json.decoder.JSONDecodeError: + pass + + return filters + + @frappe.whitelist() def get_feed_for_dt(doctype, docname): """get feed""" @@ -43,6 +58,7 @@ def get_feed_for_dt(doctype, docname): return result + @frappe.whitelist() def get_patient_history_doctypes(): document_types = [] @@ -54,4 +70,4 @@ def get_patient_history_doctypes(): for entry in settings.custom_doctypes: document_types.append(entry.document_type) - return document_types \ No newline at end of file + return document_types From 06e7cc2c35bf526f5a7bfba81d645ed4028a276b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 12:01:48 +0530 Subject: [PATCH 027/114] fix: Handle table field rendering in Patient Medical record --- .../patient_history_settings.py | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index af8c6f4557..759fcadef2 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -51,10 +51,16 @@ def set_subject_field(doc): for entry in patient_history_fields: fieldname = entry.get('fieldname') - if doc.get(fieldname): - formated_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) - subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formated_value) - subject += '
' + if entry.get('fieldtype') == 'Table' and doc.get(fieldname): + formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname)) + subject += frappe.bold(_(entry.get('label')) + ': ') + '
' + cstr(formatted_value) + + else: + if doc.get(fieldname): + formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) + subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + + subject += '
' return subject @@ -72,3 +78,27 @@ def get_patient_history_fields(doc): if patient_history_fields: return json.loads(patient_history_fields) + +def get_formatted_value_for_table_field(items, df): + child_meta = frappe.get_meta(df.options) + + table_head = '' + table_row = '' + html = '' + create_head = True + for item in items: + table_row += '' + for cdf in child_meta.fields: + if cdf.in_list_view: + if create_head: + table_head += '' + cdf.label + '' + if item.get(cdf.fieldname): + table_row += '' + str(item.get(cdf.fieldname)) + '' + else: + table_row += '' + create_head = False + table_row += '' + + html += "" + table_head + table_row + '
' + + return html From f3df5c9f3cec4db7092c301d03897eca3acc67e0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 12:16:28 +0530 Subject: [PATCH 028/114] feat: patch to setup standard doctype config in Patient History Settings --- .../healthcare/doctype/lab_test/lab_test.json | 6 +- .../patient_encounter/patient_encounter.json | 4 +- ...atient_history_standard_document_type.json | 11 ++- erpnext/healthcare/setup.py | 80 +++++++++++++++++++ erpnext/patches.txt | 1 + ..._history_settings_for_standard_doctypes.py | 9 +++ 6 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index edf1d911aa..ac61fea3ad 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -359,6 +359,7 @@ { "fieldname": "normal_test_items", "fieldtype": "Table", + "label": "Normal Test Result", "options": "Normal Test Result", "print_hide": 1 }, @@ -380,6 +381,7 @@ { "fieldname": "sensitivity_test_items", "fieldtype": "Table", + "label": "Sensitivity Test Result", "options": "Sensitivity Test Result", "print_hide": 1, "report_hide": 1 @@ -529,6 +531,7 @@ { "fieldname": "descriptive_test_items", "fieldtype": "Table", + "label": "Descriptive Test Result", "options": "Descriptive Test Result", "print_hide": 1, "report_hide": 1 @@ -549,13 +552,14 @@ { "fieldname": "organism_test_items", "fieldtype": "Table", + "label": "Organism Test Result", "options": "Organism Test Result", "print_hide": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-07-30 18:18:38.516215", + "modified": "2020-11-30 11:04:17.195848", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index 15675f4673..b646ff9ebe 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json @@ -210,7 +210,7 @@ { "fieldname": "drug_prescription", "fieldtype": "Table", - "label": "Items", + "label": "Drug Prescription", "options": "Drug Prescription" }, { @@ -328,7 +328,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-05-16 21:00:08.644531", + "modified": "2020-11-30 10:39:00.783119", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Encounter", diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json index ef4fc2bfe1..9c9d0cb4cd 100644 --- a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "document_type", + "date_fieldname", "add_edit_fields", "selected_fields" ], @@ -29,12 +30,20 @@ "fieldtype": "Button", "in_list_view": 1, "label": "Add / Edit Fields" + }, + { + "fieldname": "date_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Date Fieldname", + "read_only": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-28 18:57:30.446348", + "modified": "2020-11-30 12:15:14.940935", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Standard Document Type", diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py index 06840801d3..bf4df7e4c8 100644 --- a/erpnext/healthcare/setup.py +++ b/erpnext/healthcare/setup.py @@ -16,6 +16,7 @@ def setup_healthcare(): create_healthcare_item_groups() create_sensitivity() add_healthcare_service_unit_tree_root() + setup_patient_history_settings() def create_medical_departments(): departments = [ @@ -213,3 +214,82 @@ def get_company(): if company: return company[0].name return None + +def setup_patient_history_settings(): + import json + + settings = frappe.get_single('Patient History Settings') + configuration = get_patient_history_config() + for dt, config in configuration.items(): + settings.append("standard_doctypes", { + "document_type": dt, + "date_fieldname": config[0], + "selected_fields": json.dumps(config[1]) + }) + settings.save() + +def get_patient_history_config(): + return { + "Patient Encounter": ("encounter_date", [ + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"}, + {"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"}, + {"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"}, + {"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"}, + {"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"}, + {"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"}, + {"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"} + ]), + "Clinical Procedure": ("start_date", [ + {"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"}, + {"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"}, + {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, + {"label": "Sample", "fieldname": "sample", "fieldtype": "Link"} + ]), + "Lab Test": ("result_date", [ + {"label": "Test Template", "fieldname": "template", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"}, + {"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"}, + {"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"}, + {"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"}, + {"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"}, + {"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"}, + {"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"}, + {"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"} + ]), + "Therapy Session": ("start_date", [ + {"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"}, + {"label": "Duration", "fieldname": "duration", "fieldtype": "Int"}, + {"label": "Location", "fieldname": "location", "fieldtype": "Link"}, + {"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"}, + {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, + {"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"}, + {"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"}, + {"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"} + ]), + "Vital Signs": ("signs_date", [ + {"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"}, + {"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"}, + {"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"}, + {"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"}, + {"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"}, + {"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"}, + {"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"}, + {"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"}, + {"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"}, + {"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"}, + {"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"} + ]), + "Inpatient Medication Order": ("start_date", [ + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"}, + {"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"}, + {"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"}, + {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"} + ]) + } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be884117..fcb63ed6e4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py new file mode 100644 index 0000000000..3332be0561 --- /dev/null +++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals +import frappe +from erpnext.healthcare.setup import setup_patient_history_settings + +def execute(): + if 'Healthcare' not in frappe.get_active_domains(): + return + + setup_patient_history_settings() \ No newline at end of file From 4097e89f8b80140389eec6b29aea337d3a7835f0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 12:48:39 +0530 Subject: [PATCH 029/114] feat: Standard doctype config for Patient History --- .../patient_history_settings.js | 17 ++++++++++++-- .../patient_history_settings.py | 22 ++++++++++++++----- erpnext/hooks.py | 1 + 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index 60926eeb11..c3d0dce675 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Patient History Settings', { }); }, - field_selector: function(frm, doc) { + field_selector: function(frm, doc, standard=1) { let document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); let d = new frappe.ui.Dialog({ title: __('{0} Fields', [__(doc.document_type)]), @@ -45,8 +45,12 @@ frappe.ui.form.on('Patient History Settings', { }); } } + let doctype = 'Patient History Custom Document Type'; + if (standard) + doctype = 'Patient History Standard Document Type'; - frappe.model.set_value('Patient History Custom Document Type', doc.name, 'selected_fields', JSON.stringify(selected_fields)); + + frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); d.hide(); }); @@ -75,6 +79,15 @@ frappe.ui.form.on('Patient History Settings', { }); frappe.ui.form.on('Patient History Custom Document Type', { + add_edit_fields: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.document_type) { + frm.events.field_selector(frm, row, standard=0); + } + } +}); + +frappe.ui.form.on('Patient History Standard Document Type', { add_edit_fields: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.document_type) { diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 759fcadef2..20b062e909 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.utils import cstr from frappe.model.document import Document +from erpnext.healthcare.page.patient_history.patient_history import get_patient_history_doctypes class PatientHistorySettings(Document): def validate(self): @@ -29,6 +30,9 @@ def create_medical_record(doc, method=None): frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': return + if doc.doctype not in get_patient_history_doctypes(): + return + subject = set_subject_field(doc) date_field = get_date_field(doc.doctype) medical_record = frappe.new_doc('Patient Medical Record') @@ -66,14 +70,15 @@ def set_subject_field(doc): def get_date_field(doctype): - return frappe.db.get_value('Patient History Custom Document Type', - { 'document_type': doctype }, 'date_fieldname') + dt = get_patient_history_config_dt(doctype) + + return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname') def get_patient_history_fields(doc): import json - patient_history_fields = frappe.db.get_value('Patient History Custom Document Type', - { 'document_type': doc.doctype }, 'selected_fields') + dt = get_patient_history_config_dt(doc.doctype) + patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields') if patient_history_fields: return json.loads(patient_history_fields) @@ -99,6 +104,13 @@ def get_formatted_value_for_table_field(items, df): create_head = False table_row += '' - html += "" + table_head + table_row + '
' + html += "" + table_head + table_row + "
" return html + + +def get_patient_history_config_dt(doctype): + if frappe.db.get_value('DocType', doctype, 'custom'): + return 'Patient History Custom Document Type' + else: + return 'Patient History Standard Document Type' diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4ee42c7559..aa5291aa0e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -223,6 +223,7 @@ standard_queries = { doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", + "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" }, "Stock Entry": { From c0fcc807d338ad614aa76f7ea03d2b804760da77 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 13:22:01 +0530 Subject: [PATCH 030/114] feat: hooks for updating and deleting medical records --- .../patient_history_settings.py | 41 +++++++++++++++++-- erpnext/hooks.py | 2 +- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 20b062e909..367c34f1e8 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -26,11 +26,11 @@ class PatientHistorySettings(Document): def create_medical_record(doc, method=None): - if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: return - if doc.doctype not in get_patient_history_doctypes(): + if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }): return subject = set_subject_field(doc) @@ -46,6 +46,30 @@ def create_medical_record(doc, method=None): medical_record.save(ignore_permissions=True) +def update_medical_record(doc, method=None): + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: + return + + medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }) + + if medical_record_id: + subject = set_subject_field(doc) + frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject) + else: + create_medical_record(doc) + + +def delete_medical_record(doc, method=None): + medical_record_required = validate_medical_record_required(doc) + if not medical_record_required: + return + + record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }) + if record: + frappe.delete_doc('Patient Medical Record', record, force=1) + + def set_subject_field(doc): from frappe.utils.formatters import format_value @@ -114,3 +138,14 @@ def get_patient_history_config_dt(doctype): return 'Patient History Custom Document Type' else: return 'Patient History Standard Document Type' + + +def validate_medical_record_required(doc): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ + frappe.db.get_value('Doctype', doc.doctype, 'module') != 'Healthcare': + return False + + if doc.doctype not in get_patient_history_doctypes(): + return False + + return True \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index aa5291aa0e..51c169f400 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -223,7 +223,7 @@ standard_queries = { doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", - "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", + "on_update": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" }, "Stock Entry": { From c538a4a31d2c9f829a308e03a01d9eac5d537d0e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 13:22:34 +0530 Subject: [PATCH 031/114] refactor: remove medical record creation methods from individual doctypes --- .../clinical_procedure/clinical_procedure.py | 19 ------ .../healthcare/doctype/lab_test/lab_test.py | 56 ----------------- .../patient_encounter/patient_encounter.py | 62 +------------------ .../therapy_session/therapy_session.py | 21 ------- .../doctype/vital_signs/vital_signs.py | 40 ------------ 5 files changed, 1 insertion(+), 197 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index e55a1433a5..c324228467 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -100,7 +100,6 @@ class ClinicalProcedure(Document): allow_start = self.set_actual_qty() if allow_start: self.db_set('status', 'In Progress') - insert_clinical_procedure_to_medical_record(self) return 'success' return 'insufficient stock' @@ -247,21 +246,3 @@ def make_procedure(source_name, target_doc=None): }, target_doc, set_missing_values) return doc - - -def insert_clinical_procedure_to_medical_record(doc): - subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "
" - if doc.practitioner: - subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner - if subject and doc.notes: - subject += '
' + doc.notes - - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.start_date - medical_record.reference_doctype = 'Clinical Procedure' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions=True) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 2db7743865..4b57cd073d 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -17,11 +17,9 @@ class LabTest(Document): self.validate_result_values() self.db_set('submitted_date', getdate()) self.db_set('status', 'Completed') - insert_lab_test_to_medical_record(self) def on_cancel(self): self.db_set('status', 'Cancelled') - delete_lab_test_from_medical_record(self) self.reload() def on_update(self): @@ -330,60 +328,6 @@ def get_employee_by_user_id(user_id): return frappe.get_doc('Employee', emp_id) return None -def insert_lab_test_to_medical_record(doc): - table_row = False - subject = cstr(doc.lab_test_name) - if doc.practitioner: - subject += frappe.bold(_('Healthcare Practitioner: '))+ doc.practitioner + '
' - if doc.normal_test_items: - item = doc.normal_test_items[0] - comment = '' - if item.lab_test_comment: - comment = str(item.lab_test_comment) - table_row = frappe.bold(_('Lab Test Conducted: ')) + item.lab_test_name - - if item.lab_test_event: - table_row += frappe.bold(_('Lab Test Event: ')) + item.lab_test_event - - if item.result_value: - table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value - - if item.normal_range: - table_row += ' ' + _('Normal Range: ') + item.normal_range - table_row += ' ' + comment - - elif doc.descriptive_test_items: - item = doc.descriptive_test_items[0] - - if item.lab_test_particulars and item.result_value: - table_row = item.lab_test_particulars + ' ' + item.result_value - - elif doc.sensitivity_test_items: - item = doc.sensitivity_test_items[0] - - if item.antibiotic and item.antibiotic_sensitivity: - table_row = item.antibiotic + ' ' + item.antibiotic_sensitivity - - if table_row: - subject += '
' + table_row - if doc.lab_test_comment: - subject += '
' + cstr(doc.lab_test_comment) - - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.result_date - medical_record.reference_doctype = 'Lab Test' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions = True) - -def delete_lab_test_from_medical_record(self): - medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name)) - - if medical_record_id and medical_record_id[0][0]: - frappe.delete_doc('Patient Medical Record', medical_record_id[0][0]) @frappe.whitelist() def get_lab_test_prescribed(patient): diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 87f42491fc..cc2141790f 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -17,10 +17,6 @@ class PatientEncounter(Document): def on_update(self): if self.appointment: frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') - update_encounter_medical_record(self) - - def after_insert(self): - insert_encounter_to_medical_record(self) def on_submit(self): if self.therapies: @@ -33,8 +29,6 @@ class PatientEncounter(Document): if self.inpatient_record and self.drug_prescription: delete_ip_medication_order(self) - delete_medical_record(self) - def set_title(self): self.title = _('{0} with {1}').format(self.patient_name or self.patient, self.practitioner_name or self.practitioner)[:100] @@ -102,61 +96,7 @@ def create_therapy_plan(encounter): 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') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.encounter_date - medical_record.reference_doctype = 'Patient Encounter' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions=True) - - -def update_encounter_medical_record(encounter): - medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name}) - - if medical_record_id and medical_record_id[0][0]: - subject = set_subject_field(encounter) - frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject) - else: - insert_encounter_to_medical_record(encounter) - - -def delete_medical_record(encounter): - record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name}) - if record: - frappe.delete_doc('Patient Medical Record', record, force=1) - def delete_ip_medication_order(encounter): record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name}) if record: - frappe.delete_doc('Inpatient Medication Order', record, force=1) - - -def set_subject_field(encounter): - subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '
' - if encounter.symptoms: - subject += frappe.bold(_('Symptoms: ')) + '
' - for entry in encounter.symptoms: - subject += cstr(entry.complaint) + '
' - else: - subject += frappe.bold(_('No Symptoms')) + '
' - - if encounter.diagnosis: - subject += frappe.bold(_('Diagnosis: ')) + '
' - for entry in encounter.diagnosis: - subject += cstr(entry.diagnosis) + '
' - else: - subject += frappe.bold(_('No Diagnosis')) + '
' - - if encounter.drug_prescription: - subject += '
' + _('Drug(s) Prescribed.') - if encounter.lab_test_prescription: - subject += '
' + _('Test(s) Prescribed.') - if encounter.procedure_prescription: - subject += '
' + _('Procedure(s) Prescribed.') - - return subject + frappe.delete_doc('Inpatient Medication Order', record, force=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 index 85d0970177..f8a8e0c8a1 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -41,7 +41,6 @@ class TherapySession(Document): def on_submit(self): self.update_sessions_count_in_therapy_plan() - insert_session_medical_record(self) def on_cancel(self): self.update_sessions_count_in_therapy_plan(on_cancel=True) @@ -135,23 +134,3 @@ def get_therapy_item(therapy, item): item.reference_dt = 'Therapy Session' item.reference_dn = therapy.name return item - - -def insert_session_medical_record(doc): - subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '
' - if doc.therapy_plan: - subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '
' - if doc.practitioner: - subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner - subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '
' - subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '
' - - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.start_date - medical_record.reference_doctype = 'Therapy Session' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.save(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py index 69d81ff4b0..35c823d739 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py @@ -12,47 +12,7 @@ class VitalSigns(Document): def validate(self): self.set_title() - def on_submit(self): - insert_vital_signs_to_medical_record(self) - - def on_cancel(self): - delete_vital_signs_from_medical_record(self) - def set_title(self): self.title = _('{0} on {1}').format(self.patient_name or self.patient, frappe.utils.format_date(self.signs_date))[:100] -def insert_vital_signs_to_medical_record(doc): - subject = set_subject_field(doc) - medical_record = frappe.new_doc('Patient Medical Record') - medical_record.patient = doc.patient - medical_record.subject = subject - medical_record.status = 'Open' - medical_record.communication_date = doc.signs_date - medical_record.reference_doctype = 'Vital Signs' - medical_record.reference_name = doc.name - medical_record.reference_owner = doc.owner - medical_record.flags.ignore_mandatory = True - medical_record.save(ignore_permissions=True) - -def delete_vital_signs_from_medical_record(doc): - medical_record = frappe.db.get_value('Patient Medical Record', {'reference_name': doc.name}) - if medical_record: - frappe.delete_doc('Patient Medical Record', medical_record) - -def set_subject_field(doc): - subject = '' - if doc.temperature: - subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '
' - if doc.pulse: - subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '
' - if doc.respiratory_rate: - subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '
' - if doc.bp: - subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '
' - if doc.bmi: - subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '
' - if doc.nutrition_note: - subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '
' - - return subject From 5e3c51bf7d157a34baed3e791f8bb40052587654 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 13:35:00 +0530 Subject: [PATCH 032/114] refactor: format value using standard formatters --- erpnext/healthcare/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 96282f50a9..6b495a4eac 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import math import frappe from frappe import _ +from frappe.utils.formatters import format_value from frappe.utils import time_diff_in_hours, rounded from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity @@ -642,11 +643,15 @@ def render_doc_as_html(doctype, docname, exclude_fields = []): html += "" \ + table_head + table_row + "
" continue + #on other field types add label and value to html if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: - html += '
{0} : {1}'.format(df.label or df.fieldname, \ - doc.get(df.fieldname)) + if doc.get(df.fieldname): + formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc) + html += '
{0} : {1}'.format(df.label or df.fieldname, formatted_value) + if not has_data : has_data = True + if sec_on and col_on and has_data: doc_html += section_html + html + '
' elif sec_on and not col_on and has_data: From 4ee293d2f45c257fa4b103adc1af088e68d4dd5f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 14:12:48 +0530 Subject: [PATCH 033/114] fix: handle non-configured fields --- .../patient_history_custom_document_type.json | 5 +++-- .../patient_history_settings.js | 12 ++++++++---- .../patient_history_standard_document_type.json | 5 +++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json index 7986e48ced..3025c7b06d 100644 --- a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json +++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json @@ -22,7 +22,8 @@ { "fieldname": "selected_fields", "fieldtype": "Code", - "label": "selected_fields" + "label": "Selected Fields", + "read_only": 1 }, { "fieldname": "add_edit_fields", @@ -41,7 +42,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-28 19:04:48.323164", + "modified": "2020-11-30 13:54:37.474671", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Custom Document Type", diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js index c3d0dce675..ee363462ef 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js @@ -14,7 +14,11 @@ frappe.ui.form.on('Patient History Settings', { }, field_selector: function(frm, doc, standard=1) { - let document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + let document_fields = []; + if (doc.selected_fields) + document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname); + + let doctype_fields = frm.events.get_doctype_fields(frm, doc.document_type, document_fields); let d = new frappe.ui.Dialog({ title: __('{0} Fields', [__(doc.document_type)]), fields: [ @@ -22,7 +26,7 @@ frappe.ui.form.on('Patient History Settings', { label: __('Select Fields'), fieldtype: 'MultiCheck', fieldname: 'fields', - options: frm.events.get_doctype_fields(frm, doc.document_type, document_fields), + options: doctype_fields, columns: 2 } ] @@ -49,7 +53,7 @@ frappe.ui.form.on('Patient History Settings', { if (standard) doctype = 'Patient History Standard Document Type'; - + d.refresh(); frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields)); d.hide(); }); @@ -82,7 +86,7 @@ frappe.ui.form.on('Patient History Custom Document Type', { add_edit_fields: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.document_type) { - frm.events.field_selector(frm, row, standard=0); + frm.events.field_selector(frm, row, 0); } } }); diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json index 9c9d0cb4cd..b43099c4ea 100644 --- a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json +++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json @@ -23,7 +23,8 @@ { "fieldname": "selected_fields", "fieldtype": "Code", - "label": "Selected Fields" + "label": "Selected Fields", + "read_only": 1 }, { "fieldname": "add_edit_fields", @@ -43,7 +44,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-30 12:15:14.940935", + "modified": "2020-11-30 13:54:56.773325", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient History Standard Document Type", From ed3fc20731ae7e2279fabf6274a7660ca520a2e7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 Nov 2020 14:56:24 +0530 Subject: [PATCH 034/114] fix: sider issues --- .../page/patient_history/patient_history.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 5a6295b707..d509ea22a2 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -24,7 +24,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { if (pid != patient_id && patient_id) { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - setup_filters(patient_id, me) + setup_filters(patient_id, me); get_documents(patient_id, me); show_patient_info(patient_id, me); show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure'); @@ -90,7 +90,7 @@ frappe.pages['patient_history'].on_page_load = function(wrapper) { me.page.main.find('.' + docname).parent().find('.document-html').hide(); }); me.start = 0; - me.page.main.on('click', '.btn-get-records', function(){ + me.page.main.on('click', '.btn-get-records', function() { get_documents(patient.get_value(), me); }); }; @@ -142,7 +142,7 @@ let setup_filters = function(patient, me) { }); date_range_field.refresh(); }); -} +}; let get_documents = function(patient, me, document_types="", selected_date_range="") { let filters = { @@ -176,7 +176,7 @@ let get_documents = function(patient, me, document_types="", selected_date_range let add_to_records = function(me, data) { let details = "