diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py index cd8e204f4c8..2ba21397ad0 100644 --- a/erpnext/accounts/doctype/finance_book/test_finance_book.py +++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py @@ -9,19 +9,8 @@ import frappe import unittest class TestFinanceBook(unittest.TestCase): - def create_finance_book(self): - if not frappe.db.exists("Finance Book", "_Test Finance Book"): - finance_book = frappe.get_doc({ - "doctype": "Finance Book", - "finance_book_name": "_Test Finance Book" - }).insert() - else: - finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") - - return finance_book - def test_finance_book(self): - finance_book = self.create_finance_book() + finance_book = create_finance_book() # create jv entry jv = make_journal_entry("_Test Bank - _TC", @@ -41,3 +30,14 @@ class TestFinanceBook(unittest.TestCase): for gl_entry in gl_entries: self.assertEqual(gl_entry.finance_book, finance_book.name) + +def create_finance_book(): + if not frappe.db.exists("Finance Book", "_Test Finance Book"): + finance_book = frappe.get_doc({ + "doctype": "Finance Book", + "finance_book_name": "_Test Finance Book" + }).insert() + else: + finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") + + return finance_book \ No newline at end of file diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 2f9bf8b07eb..16fc98b38c2 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -50,9 +50,13 @@ class PeriodClosingVoucher(AccountsController): .format(pce[0][0], self.posting_date)) def make_gl_entries(self): + gl_entries = self.get_gl_entries() + if gl_entries: + from erpnext.accounts.general_ledger import make_gl_entries + make_gl_entries(gl_entries) + + def get_gl_entries(self): gl_entries = [] - net_pl_balance = 0 - pl_accounts = self.get_pl_balances() for acc in pl_accounts: @@ -60,6 +64,7 @@ class PeriodClosingVoucher(AccountsController): gl_entries.append(self.get_gl_dict({ "account": acc.account, "cost_center": acc.cost_center, + "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, @@ -67,35 +72,13 @@ class PeriodClosingVoucher(AccountsController): "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 }, item=acc)) - net_pl_balance += flt(acc.bal_in_company_currency) + if gl_entries: + gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts) + gl_entries += gle_for_net_pl_bal - if net_pl_balance: - if self.cost_center_wise_pnl: - costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts) - gl_entries += costcenter_wise_gl_entries - else: - gl_entry = self.get_pnl_gl_entry(net_pl_balance) - gl_entries.append(gl_entry) - - from erpnext.accounts.general_ledger import make_gl_entries - make_gl_entries(gl_entries) - - def get_pnl_gl_entry(self, net_pl_balance): - cost_center = frappe.db.get_value("Company", self.company, "cost_center") - gl_entry = self.get_gl_dict({ - "account": self.closing_account_head, - "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "cost_center": cost_center - }) - - self.update_default_dimensions(gl_entry) - - return gl_entry - - def get_costcenter_wise_pnl_gl_entries(self, pl_accounts): + return gl_entries + + def get_pnl_gl_entry(self, pl_accounts): company_cost_center = frappe.db.get_value("Company", self.company, "cost_center") gl_entries = [] @@ -104,6 +87,7 @@ class PeriodClosingVoucher(AccountsController): gl_entry = self.get_gl_dict({ "account": self.closing_account_head, "cost_center": acc.cost_center or company_cost_center, + "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, @@ -130,7 +114,7 @@ class PeriodClosingVoucher(AccountsController): def get_pl_balances(self): """Get balance for dimension-wise pl accounts""" - dimension_fields = ['t1.cost_center'] + dimension_fields = ['t1.cost_center', 't1.finance_book'] self.accounting_dimensions = get_accounting_dimensions() for dimension in self.accounting_dimensions: diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index f17a5c51a08..2d1939131c3 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import flt, today from erpnext.accounts.utils import get_fiscal_year, now from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice class TestPeriodClosingVoucher(unittest.TestCase): @@ -118,6 +119,58 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertTrue(pcv_gle, expected_gle) + def test_period_closing_with_finance_book_entries(self): + frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + + company = create_company() + surplus_account = create_account() + cost_center = create_cost_center("Test Cost Center 1") + + create_sales_invoice( + company=company, + income_account="Sales - TPC", + expense_account="Cost of Goods Sold - TPC", + cost_center=cost_center, + rate=400, + debit_to="Debtors - TPC" + ) + jv = make_journal_entry( + account1="Cash - TPC", + account2="Sales - TPC", + amount=400, + cost_center=cost_center, + posting_date=now() + ) + jv.company = company + jv.finance_book = create_finance_book().name + jv.save() + jv.submit() + + pcv = frappe.get_doc({ + "transaction_date": today(), + "posting_date": today(), + "fiscal_year": get_fiscal_year(today())[0], + "company": company, + "closing_account_head": surplus_account, + "remarks": "Test", + "doctype": "Period Closing Voucher" + }) + pcv.insert() + pcv.submit() + + expected_gle = ( + (surplus_account, 0.0, 400.0, ''), + (surplus_account, 0.0, 400.0, jv.finance_book), + ('Sales - TPC', 400.0, 0.0, ''), + ('Sales - TPC', 400.0, 0.0, jv.finance_book) + ) + + pcv_gle = frappe.db.sql(""" + select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s + """, (pcv.name)) + + self.assertTrue(pcv_gle, expected_gle) + def make_period_closing_voucher(self): pcv = frappe.get_doc({ "doctype": "Period Closing Voucher", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 04e392a0f27..33f04cbaa63 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -110,17 +110,13 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend( this.frm.refresh_field("base_paid_amount"); }, - write_off_outstanding_amount_automatically: function() { - if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { + write_off_outstanding_amount_automatically() { + if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); // this will make outstanding amount 0 this.frm.set_value("write_off_amount", flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) ); - this.frm.toggle_enable("write_off_amount", false); - - } else { - this.frm.toggle_enable("write_off_amount", true); } this.calculate_outstanding_amount(false); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index fcccb39b70c..19c6c8f3472 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -99,6 +99,7 @@ "loyalty_redemption_account", "loyalty_redemption_cost_center", "section_break_49", + "coupon_code", "apply_discount_on", "base_discount_amount", "column_break_51", @@ -1183,7 +1184,8 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval: doc.write_off_outstanding_amount_automatically" }, { "fieldname": "base_write_off_amount", @@ -1549,12 +1551,20 @@ "no_copy": 1, "options": "Sales Invoice", "read_only": 1 + }, + { + "depends_on": "coupon_code", + "fieldname": "coupon_code", + "fieldtype": "Link", + "label": "Coupon Code", + "options": "Coupon Code", + "print_hide": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-17 20:13:44.255437", + "modified": "2021-08-24 18:19:20.728433", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8ec4ef224cd..034a217a26d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -44,6 +44,9 @@ class POSInvoice(SalesInvoice): self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program @@ -58,6 +61,10 @@ class POSInvoice(SalesInvoice): self.check_phone_payments() self.set_status(update=True) + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + update_coupon_code_count(self.coupon_code,'used') + def before_cancel(self): if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: pos_closing_entry = frappe.get_all( @@ -84,6 +91,10 @@ class POSInvoice(SalesInvoice): against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + update_coupon_code_count(self.coupon_code,'cancelled') + def check_phone_payments(self): for pay in self.payments: if pay.type == "Phone" and pay.amount >= 0: @@ -127,7 +138,7 @@ class POSInvoice(SalesInvoice): .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) def validate_stock_availablility(self): - if self.is_return: + if self.is_return or self.docstatus != 1: return allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 6172796129f..d2527fb2e50 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -320,7 +320,8 @@ class TestPOSInvoice(unittest.TestCase): pos2.get("items")[0].serial_no = serial_nos[0] pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) - self.assertRaises(frappe.ValidationError, pos2.insert) + pos2.insert() + self.assertRaises(frappe.ValidationError, pos2.submit) def test_delivered_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -348,7 +349,8 @@ class TestPOSInvoice(unittest.TestCase): pos2.get("items")[0].serial_no = serial_nos[0] pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) - self.assertRaises(frappe.ValidationError, pos2.insert) + pos2.insert() + self.assertRaises(frappe.ValidationError, pos2.submit) def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 556f49d34c0..4903c50e17b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -198,12 +198,19 @@ def apply_pricing_rule(args, doc=None): set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo") + item_code_list = tuple(item.get('item_code') for item in item_list) + query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1) + serialized_items = dict() + for item_code, val in query_items: + serialized_items.setdefault(item_code, val) + for item in item_list: args_copy = copy.deepcopy(args) args_copy.update(item) data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc) out.append(data) - if not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): + + if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): out[0].update(get_serial_no_for_item(args_copy)) return out diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 568e7721a39..a98a83a0476 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -323,17 +323,13 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.frm.refresh_fields(); }, - write_off_outstanding_amount_automatically: function() { - if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { + write_off_outstanding_amount_automatically() { + if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); // this will make outstanding amount 0 this.frm.set_value("write_off_amount", flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) ); - this.frm.toggle_enable("write_off_amount", false); - - } else { - this.frm.toggle_enable("write_off_amount", true); } this.calculate_outstanding_amount(false); @@ -787,8 +783,6 @@ frappe.ui.form.on('Sales Invoice', { if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']); else hide_field(['c_form_applicable', 'c_form_no']); - frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically)); - frm.refresh_fields(); }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4c7a6b51ac7..01ae713cd36 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1444,7 +1444,8 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval:doc.write_off_outstanding_amount_automatically" }, { "fieldname": "base_write_off_amount", @@ -2016,7 +2017,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-08-17 20:16:12.737743", + "modified": "2021-08-18 16:07:45.122570", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/south_africa_vat_account/__init__.py b/erpnext/accounts/doctype/south_africa_vat_account/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json new file mode 100644 index 00000000000..fa1aa7da594 --- /dev/null +++ b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "autoname": "account", + "creation": "2021-07-08 22:04:24.634967", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "label": "Account", + "options": "Account" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-07-08 22:35:33.202911", + "modified_by": "Administrator", + "module": "Accounts", + "name": "South Africa VAT Account", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py new file mode 100644 index 00000000000..4bd8c65a046 --- /dev/null +++ b/erpnext/accounts/doctype/south_africa_vat_account/south_africa_vat_account.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class SouthAfricaVATAccount(Document): + pass diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 4c7c567b42a..31261384080 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None): def check_if_in_list(gle, gl_map, dimensions=None): account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher', - 'cost_center', 'against_voucher_type', 'party_type', 'project'] + 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 5d8d49d6a65..3723c8e0d23 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -78,13 +78,10 @@ def validate_filters(filters, account_details): def validate_party(filters): party_type, party = filters.get("party_type"), filters.get("party") - if party: - if not party_type: - frappe.throw(_("To filter based on Party, select Party Type first")) - else: - for d in party: - if not frappe.db.exists(party_type, d): - frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + if party and party_type: + for d in party: + if not frappe.db.exists(party_type, d): + frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) def set_account_currency(filters): if filters.get("account") or (filters.get('party') and len(filters.party) == 1): diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index cd6bac2d77d..5fff3fdba77 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,16 +1,20 @@ { - "add_total_row": 1, + "add_total_row": 0, + "columns": [], "creation": "2013-02-25 17:03:34", + "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2020-08-13 11:26:39.112352", + "modified": "2021-08-19 18:57:07.468202", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", "owner": "Administrator", + "prepared_report": 0, "ref_doctype": "Sales Invoice", "report_name": "Gross Profit", "report_type": "Script Report", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 6d8623c189d..c949d9b74e5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -41,12 +41,14 @@ def execute(filters=None): columns = get_columns(group_wise_columns, filters) - for src in gross_profit_data.grouped_data: + for idx, src in enumerate(gross_profit_data.grouped_data): row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) + if idx == len(gross_profit_data.grouped_data)-1: + row[0] = frappe.bold("Total") data.append(row) return columns, data @@ -154,6 +156,15 @@ class GrossProfitGenerator(object): def get_average_rate_based_on_group_by(self): # sum buying / selling totals for group + self.totals = frappe._dict( + qty=0, + base_amount=0, + buying_amount=0, + gross_profit=0, + gross_profit_percent=0, + base_rate=0, + buying_rate=0 + ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): @@ -165,6 +176,7 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) + self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.parent in self.returned_invoices \ @@ -177,15 +189,25 @@ class GrossProfitGenerator(object): if row.qty or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) + self.add_to_totals(row) + self.set_average_gross_profit(self.totals) + self.grouped_data.append(self.totals) def set_average_rate(self, new_row): + self.set_average_gross_profit(new_row) + new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + return new_row + + def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ if new_row.base_amount else 0 - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - return new_row + def add_to_totals(self, new_row): + for key in self.totals: + if new_row.get(key): + self.totals[key] += new_row[key] def get_returned_invoice_items(self): returned_invoices = frappe.db.sql(""" diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index 10a4001502f..7e3ecaf3ab6 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -588,7 +588,7 @@ { "hidden": 0, "is_query_report": 0, - "label": "Bank Statement", + "label": "Banking and Payments", "onboard": 0, "type": "Card Break" }, @@ -642,6 +642,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Entry", + "link_to": "Payment Entry", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -1064,7 +1082,7 @@ "type": "Link" } ], - "modified": "2021-06-10 03:17:31.427945", + "modified": "2021-08-23 16:06:34.167267", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 4ddc458175b..1766c2c80cc 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Supplier Group Details'), function () { + frm.trigger("get_supplier_group_details"); + }, __('Actions')); + // indicators erpnext.utils.set_party_dashboard_indicators(frm); } }, + get_supplier_group_details: function(frm) { + frappe.call({ + method: "get_supplier_group_details", + doc: frm.doc, + callback: function() { + frm.refresh(); + } + }); + }, is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 04eb4c04c00..fd16b23c220 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -51,6 +51,23 @@ class Supplier(TransactionBase): validate_party_accounts(self) self.validate_internal_supplier() + @frappe.whitelist() + def get_supplier_group_details(self): + doc = frappe.get_doc('Supplier Group', self.supplier_group) + self.payment_terms = "" + self.accounts = [] + + if doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + + if doc.payment_terms: + self.payment_terms = doc.payment_terms + + self.save() + def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 039a27dd6e2..89804662700 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -13,6 +13,30 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): + def test_get_supplier_group_details(self): + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() + def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date frappe.db.set_value( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 5e79eb6fae8..b17d1868d99 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -164,7 +164,8 @@ class AccountsController(TransactionBase): self.set_due_date() self.set_payment_schedule() self.validate_payment_schedule_amount() - self.validate_due_date() + if not self.get('ignore_default_payment_terms_template'): + self.validate_due_date() self.validate_advance_entries() def validate_non_invoice_documents_schedule(self): @@ -1841,6 +1842,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil for d in data: new_child_flag = False + + if not d.get("item_code"): + # ignore empty rows + continue + if not d.get("docname"): new_child_flag = True check_doc_permissions(parent, 'create') diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 80ccc6d75b2..5ee1f2f7fb5 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -329,7 +329,6 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name - target_doc.price_list_rate = 0 elif doctype == "Delivery Note": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) @@ -360,7 +359,6 @@ def make_return_doc(doctype, source_name, target_doc=None): else: target_doc.pos_invoice_item = source_doc.name - target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index da2765deded..fc2cc97e0a5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime -from frappe import _, throw +from frappe import _, bold, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate from erpnext.stock.get_item_details import get_conversion_factor @@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController from erpnext.controllers.sales_and_purchase_return import get_rate_for_return class SellingController(StockController): - def get_feed(self): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) @@ -169,39 +168,96 @@ class SellingController(StockController): def validate_selling_price(self): def throw_message(idx, item_name, rate, ref_rate_field): - bold_net_rate = frappe.bold("net rate") - msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""") - .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate))) - msg += "

" - msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""") - .format(get_link_to_form("Selling Settings", "Selling Settings"))) - frappe.throw(msg, title=_("Invalid Selling Price")) + throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}. + Selling {3} should be atleast {4}.

Alternatively, + you can disable selling price validation in {5} to bypass + this validation.""").format( + idx, + bold(item_name), + bold(ref_rate_field), + bold("net rate"), + bold(rate), + get_link_to_form("Selling Settings", "Selling Settings"), + ), title=_("Invalid Selling Price")) - if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): - return - if hasattr(self, "is_return") and self.is_return: + if ( + self.get("is_return") + or not frappe.db.get_single_value("Selling Settings", "validate_selling_price") + ): return - for it in self.get("items"): - if not it.item_code: + is_internal_customer = self.get('is_internal_customer') + valuation_rate_map = {} + + for item in self.items: + if not item.item_code: continue - last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) - last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) - if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): - throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") + last_purchase_rate, is_stock_item = frappe.get_cached_value( + "Item", item.item_code, ("last_purchase_rate", "is_stock_item") + ) - last_valuation_rate = frappe.db.sql(""" - SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s - AND warehouse = %s AND valuation_rate > 0 - ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1 - """, (it.item_code, it.warehouse)) - if last_valuation_rate: - last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1) - if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \ - and not self.get('is_internal_customer'): - throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") + last_purchase_rate_in_sales_uom = ( + last_purchase_rate * (item.conversion_factor or 1) + ) + if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom): + throw_message( + item.idx, + item.item_name, + last_purchase_rate_in_sales_uom, + "last purchase rate" + ) + + if is_internal_customer or not is_stock_item: + continue + + valuation_rate_map[(item.item_code, item.warehouse)] = None + + if not valuation_rate_map: + return + + or_conditions = ( + f"""(item_code = {frappe.db.escape(valuation_rate[0])} + and warehouse = {frappe.db.escape(valuation_rate[1])})""" + for valuation_rate in valuation_rate_map + ) + + valuation_rates = frappe.db.sql(f""" + select + item_code, warehouse, valuation_rate + from + `tabBin` + where + ({" or ".join(or_conditions)}) + and valuation_rate > 0 + """, as_dict=True) + + for rate in valuation_rates: + valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate + + for item in self.items: + if not item.item_code: + continue + + last_valuation_rate = valuation_rate_map.get( + (item.item_code, item.warehouse) + ) + + if not last_valuation_rate: + continue + + last_valuation_rate_in_sales_uom = ( + last_valuation_rate * (item.conversion_factor or 1) + ) + + if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): + throw_message( + item.idx, + item.item_name, + last_valuation_rate_in_sales_uom, + "valuation rate" + ) def get_item_list(self): il = [] diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index 263005ef6c5..7aa0b777596 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -2,8 +2,8 @@ // For license information, please see license.txt frappe.ui.form.on('LinkedIn Settings', { - onload: function(frm){ - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + onload: function(frm) { + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', { } ); } + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); }, - refresh: function(frm){ + refresh: function(frm) { if (frm.doc.session_status=="Expired"){ let msg = __("Session Not Active. Save doc to login."); frm.dashboard.set_headline_alert( @@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', { ); } }, - login: function(frm){ + login: function(frm) { if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.dom.freeze(); frappe.call({ @@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', { }); } }, - after_save: function(frm){ + after_save: function(frm) { frm.trigger("login"); } }); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json index 9eacb0011c5..f882e36c32a 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2020-01-30 13:36:39.492931", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings", "editable_grid": 1, "engine": "InnoDB", "field_order": [ @@ -87,7 +88,7 @@ ], "issingle": 1, "links": [], - "modified": "2020-04-16 23:22:51.966397", + "modified": "2021-02-18 15:19:21.920725", "modified_by": "Administrator", "module": "CRM", "name": "LinkedIn Settings", diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index d8c6fb4f90f..9b88d78c1ff 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -3,11 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, requests, json +import frappe +import requests from frappe import _ -from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.utils import get_url_to_form from frappe.model.document import Document -from frappe.utils.file_manager import get_file, get_file_path +from frappe.utils.file_manager import get_file_path from six.moves.urllib.parse import urlencode class LinkedInSettings(Document): @@ -42,11 +43,7 @@ class LinkedInSettings(Document): self.db_set("access_token", response["access_token"]) def get_member_profile(self): - headers = { - "Authorization": "Bearer {}".format(self.access_token) - } - url = "https://api.linkedin.com/v2/me" - response = requests.get(url=url, headers=headers) + response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers()) response = frappe.parse_json(response.content.decode()) frappe.db.set_value(self.doctype, self.name, { @@ -55,16 +52,16 @@ class LinkedInSettings(Document): "session_status": "Active" }) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") - def post(self, text, media=None): + def post(self, text, title, media=None): if not media: - return self.post_text(text) + return self.post_text(text, title) else: media_id = self.upload_image(media) if media_id: - return self.post_text(text, media_id=media_id) + return self.post_text(text, title, media_id=media_id) else: frappe.log_error("Failed to upload media.","LinkedIn Upload Error") @@ -82,9 +79,7 @@ class LinkedInSettings(Document): }] } } - headers = { - "Authorization": "Bearer {}".format(self.access_token) - } + headers = self.get_headers() response = self.http_post(url=register_url, body=body, headers=headers) if response.status_code == 200: @@ -100,24 +95,33 @@ class LinkedInSettings(Document): return None - def post_text(self, text, media_id=None): + def post_text(self, text, title, media_id=None): url = "https://api.linkedin.com/v2/shares" - headers = { - "X-Restli-Protocol-Version": "2.0.0", - "Authorization": "Bearer {}".format(self.access_token), - "Content-Type": "application/json; charset=UTF-8" - } + headers = self.get_headers() + headers["X-Restli-Protocol-Version"] = "2.0.0" + headers["Content-Type"] = "application/json; charset=UTF-8" + body = { "distribution": { "linkedInDistributionTarget": {} }, "owner":"urn:li:organization:{0}".format(self.company_id), - "subject": "Test Share Subject", + "subject": title, "text": { "text": text } } + reference_url = self.get_reference_url(text) + if reference_url: + body["content"] = { + "contentEntities": [ + { + "entityLocation": reference_url + } + ] + } + if media_id: body["content"]= { "contentEntities": [{ @@ -141,20 +145,60 @@ class LinkedInSettings(Document): raise except Exception as e: - content = json.loads(response.content) - - if response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") - elif response.status_code == 403: - frappe.msgprint(_("You Didn't have permission to access this API")) - frappe.throw(content["message"], title="LinkedIn Error - Access Denied") - else: - frappe.throw(response.reason, title=response.status_code) - + self.api_error(response) + return response + def get_headers(self): + return { + "Authorization": "Bearer {}".format(self.access_token) + } + + def get_reference_url(self, text): + import re + regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" + urls = re.findall(regex_url, text) + if urls: + return urls[0] + + def delete_post(self, post_id): + try: + response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers()) + if response.status_code !=200: + raise + except Exception: + self.api_error(response) + + def get_post(self, post_id): + url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id) + + try: + response = requests.get(url=url, headers=self.get_headers()) + if response.status_code !=200: + raise + + except Exception: + self.api_error(response) + + response = frappe.parse_json(response.content.decode()) + if len(response.elements): + return response.elements[0] + + return None + + def api_error(self, response): + content = frappe.parse_json(response.content.decode()) + + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized")) + elif response.status_code == 403: + frappe.msgprint(_("You didn't have permission to access this API")) + frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied")) + else: + frappe.throw(response.reason, title=response.status_code) + @frappe.whitelist(allow_guest=True) def callback(code=None, error=None, error_description=None): if not error: diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 6fb0f975f46..a8f5deea535 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -1,67 +1,139 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.ui.form.on('Social Media Post', { - validate: function(frm){ - if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){ - frappe.throw(__("Select atleast one Social Media from Share on.")) - } - if (frm.doc.scheduled_time) { - let scheduled_time = new Date(frm.doc.scheduled_time); - let date_time = new Date(); - if (scheduled_time.getTime() < date_time.getTime()){ - frappe.throw(__("Invalid Scheduled Time")); - } - } - if (frm.doc.text?.length > 280){ - frappe.throw(__("Length Must be less than 280.")) - } - }, - refresh: function(frm){ - if (frm.doc.docstatus === 1){ - if (frm.doc.post_status != "Posted"){ - add_post_btn(frm); - } - else if (frm.doc.post_status == "Posted"){ - frm.set_df_property('sheduled_time', 'read_only', 1); - } + validate: function(frm) { + if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) { + frappe.throw(__("Select atleast one Social Media Platform to Share on.")); + } + if (frm.doc.scheduled_time) { + let scheduled_time = new Date(frm.doc.scheduled_time); + let date_time = new Date(); + if (scheduled_time.getTime() < date_time.getTime()) { + frappe.throw(__("Scheduled Time must be a future time.")); + } + } + frm.trigger('validate_tweet_length'); + }, - let html=''; - if (frm.doc.twitter){ - let color = frm.doc.twitter_post_id ? "green" : "red"; - let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; - html += `
- Twitter : ${status} -
` ; - } - if (frm.doc.linkedin){ - let color = frm.doc.linkedin_post_id ? "green" : "red"; - let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; - html += `
- LinkedIn : ${status} -
` ; - } - html = `
${html}
`; - frm.dashboard.set_headline_alert(html); - } - } + text: function(frm) { + if (frm.doc.text) { + frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`); + frm.refresh_field('text'); + frm.trigger('validate_tweet_length'); + } + }, + + validate_tweet_length: function(frm) { + if (frm.doc.text && frm.doc.text.length > 280) { + frappe.throw(__("Tweet length Must be less than 280.")); + } + }, + + onload: function(frm) { + frm.trigger('make_dashboard'); + }, + + make_dashboard: function(frm) { + if (frm.doc.post_status == "Posted") { + frappe.call({ + doc: frm.doc, + method: 'get_post', + freeze: true, + callback: (r) => { + if (!r.message) { + return; + } + + let datasets = [], colors = []; + if (r.message && r.message.twitter) { + colors.push('#1DA1F2'); + datasets.push({ + name: 'Twitter', + values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count] + }); + } + if (r.message && r.message.linkedin) { + colors.push('#0077b5'); + datasets.push({ + name: 'LinkedIn', + values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount] + }); + } + + if (datasets.length) { + frm.dashboard.render_graph({ + data: { + labels: ['Likes', 'Retweets/Shares'], + datasets: datasets + }, + + title: __("Post Metrics"), + type: 'bar', + height: 300, + colors: colors + }); + } + } + }); + } + }, + + refresh: function(frm) { + frm.trigger('text'); + + if (frm.doc.docstatus === 1) { + if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) { + frm.trigger('add_post_btn'); + } + if (frm.doc.post_status !='Deleted') { + frm.add_custom_button(('Delete Post'), function() { + frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'), + function() { + frappe.call({ + doc: frm.doc, + method: 'delete_post', + freeze: true, + callback: () => { + frm.reload_doc(); + } + }); + } + ); + }); + } + + if (frm.doc.post_status !='Deleted') { + let html=''; + if (frm.doc.twitter) { + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += `
+ Twitter : ${status} +
` ; + } + if (frm.doc.linkedin) { + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += `
+ LinkedIn : ${status} +
` ; + } + html = `
${html}
`; + frm.dashboard.set_headline_alert(html); + } + } + }, + + add_post_btn: function(frm) { + frm.add_custom_button(__('Post Now'), function() { + frappe.call({ + doc: frm.doc, + method: 'post', + freeze: true, + callback: function() { + frm.reload_doc(); + } + }); + }); + } }); -var add_post_btn = function(frm){ - frm.add_custom_button(('Post Now'), function(){ - post(frm); - }); -} -var post = function(frm){ - frappe.dom.freeze(); - frappe.call({ - method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", - args: { - doctype: frm.doc.doctype, - name: frm.doc.name - }, - callback: function(r) { - frm.reload_doc(); - frappe.dom.unfreeze(); - } - }) - -} diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index 0a00dca2808..98e78f949e8 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -3,9 +3,11 @@ "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", "creation": "2020-01-30 11:53:13.872864", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "title", "campaign_name", "scheduled_time", "post_status", @@ -30,32 +32,24 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "mandatory_depends_on": "eval:doc.twitter ==1", - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.twitter ==1" }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image", - "show_days": 1, - "show_seconds": 1 + "label": "Image" }, { - "default": "0", + "default": "1", "fieldname": "twitter", "fieldtype": "Check", - "label": "Twitter", - "show_days": 1, - "show_seconds": 1 + "label": "Twitter" }, { - "default": "0", + "default": "1", "fieldname": "linkedin", "fieldtype": "Check", - "label": "LinkedIn", - "show_days": 1, - "show_seconds": 1 + "label": "LinkedIn" }, { "fieldname": "amended_from", @@ -64,27 +58,22 @@ "no_copy": 1, "options": "Social Media Post", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", - "label": "Twitter", - "show_days": 1, - "show_seconds": 1 + "label": "Twitter" }, { "allow_on_submit": 1, "fieldname": "post_status", "fieldtype": "Select", "label": "Post Status", - "options": "\nScheduled\nPosted\nError", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError", + "read_only": 1 }, { "allow_on_submit": 1, @@ -92,9 +81,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Twitter Post Id", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "read_only": 1 }, { "allow_on_submit": 1, @@ -102,82 +90,69 @@ "fieldtype": "Data", "hidden": 1, "label": "LinkedIn Post Id", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "campaign_name", "fieldtype": "Link", "in_list_view": 1, "label": "Campaign", - "options": "Campaign", - "show_days": 1, - "show_seconds": 1 + "options": "Campaign" }, { "fieldname": "column_break_6", "fieldtype": "Column Break", - "label": "Share On", - "show_days": 1, - "show_seconds": 1 + "label": "Share On" }, { "fieldname": "column_break_14", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "tweet_preview", - "fieldtype": "HTML", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "HTML" }, { "collapsible": 1, "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", - "label": "LinkedIn", - "show_days": 1, - "show_seconds": 1 + "label": "LinkedIn" }, { "collapsible": 1, "fieldname": "attachments_section", "fieldtype": "Section Break", - "label": "Attachments", - "show_days": 1, - "show_seconds": 1 + "label": "Attachments" }, { "fieldname": "linkedin_post", "fieldtype": "Text", "label": "Post", - "mandatory_depends_on": "eval:doc.linkedin ==1", - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.linkedin ==1" }, { "fieldname": "column_break_15", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, "fieldname": "scheduled_time", "fieldtype": "Datetime", "label": "Scheduled Time", - "read_only_depends_on": "eval:doc.post_status == \"Posted\"", - "show_days": 1, - "show_seconds": 1 + "read_only_depends_on": "eval:doc.post_status == \"Posted\"" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-14 10:31:33.961381", + "modified": "2021-04-14 14:24:59.821223", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", @@ -228,5 +203,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index ed1b5839446..95320bff535 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -10,17 +10,51 @@ import datetime class SocialMediaPost(Document): def validate(self): + if (not self.twitter and not self.linkedin): + frappe.throw(_("Select atleast one Social Media Platform to Share on.")) + if self.scheduled_time: current_time = frappe.utils.now_datetime() scheduled_time = frappe.utils.get_datetime(self.scheduled_time) if scheduled_time < current_time: - frappe.throw(_("Invalid Scheduled Time")) + frappe.throw(_("Scheduled Time must be a future time.")) + + if self.text and len(self.text) > 280: + frappe.throw(_("Tweet length must be less than 280.")) def submit(self): if self.scheduled_time: self.post_status = "Scheduled" super(SocialMediaPost, self).submit() + + def on_cancel(self): + self.db_set('post_status', 'Cancelled') + @frappe.whitelist() + def delete_post(self): + if self.twitter and self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter.delete_tweet(self.twitter_post_id) + + if self.linkedin and self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin.delete_post(self.linkedin_post_id) + + self.db_set('post_status', 'Deleted') + + @frappe.whitelist() + def get_post(self): + response = {} + if self.linkedin and self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + response['linkedin'] = linkedin.get_post(self.linkedin_post_id) + if self.twitter and self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + response['twitter'] = twitter.get_tweet(self.twitter_post_id) + + return response + + @frappe.whitelist() def post(self): try: if self.twitter and not self.twitter_post_id: @@ -29,28 +63,22 @@ class SocialMediaPost(Document): self.db_set("twitter_post_id", twitter_post.id) if self.linkedin and not self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") - linkedin_post = linkedin.post(self.linkedin_post, self.image) - self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id']) self.db_set("post_status", "Posted") except: self.db_set("post_status", "Error") title = _("Error while POSTING {0}").format(self.name) - traceback = frappe.get_traceback() - frappe.log_error(message=traceback , title=title) + frappe.log_error(message=frappe.get_traceback(), title=title) def process_scheduled_social_media_posts(): - posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"]) + posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time"]) start = frappe.utils.now_datetime() end = start + datetime.timedelta(minutes=10) for post in posts: if post.scheduled_time: post_time = frappe.utils.get_datetime(post.scheduled_time) if post_time > start and post_time <= end: - publish('Social Media Post', post.name) - -@frappe.whitelist() -def publish(doctype, name): - sm_post = frappe.get_doc(doctype, name) - sm_post.post() - frappe.db.commit() + sm_post = frappe.get_doc('Social Media Post', post.name) + sm_post.post() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js index c60b91a9a02..a8c8272ad08 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -1,10 +1,11 @@ frappe.listview_settings['Social Media Post'] = { - add_fields: ["status","post_status"], - get_indicator: function(doc) { - return [__(doc.post_status), { - "Scheduled": "orange", - "Posted": "green", - "Error": "red" - }[doc.post_status]]; - } + add_fields: ["status", "post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red", + "Deleted": "red" + }[doc.post_status]]; + } } diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index f6f431ca5c9..112f3d4d1c3 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Twitter Settings', { - onload: function(frm){ + onload: function(frm) { if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), @@ -14,10 +14,11 @@ frappe.ui.form.on('Twitter Settings', { } ); } + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); }, - refresh: function(frm){ + refresh: function(frm) { let msg, color, flag=false; - if (frm.doc.session_status == "Active"){ + if (frm.doc.session_status == "Active") { msg = __("Session Active"); color = 'green'; flag = true; @@ -28,7 +29,7 @@ frappe.ui.form.on('Twitter Settings', { flag = true; } - if (flag){ + if (flag) { frm.dashboard.set_headline_alert( `
@@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', { ); } }, - login: function(frm){ + login: function(frm) { if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.dom.freeze(); frappe.call({ @@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', { }); } }, - after_save: function(frm){ + after_save: function(frm) { frm.trigger("login"); } }); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json index 36776e5c202..8d05877f060 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2020-01-30 10:29:08.562108", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings", "editable_grid": 1, "engine": "InnoDB", "field_order": [ @@ -77,7 +78,7 @@ "image_field": "profile_pic", "issingle": 1, "links": [], - "modified": "2020-05-13 17:50:47.934776", + "modified": "2021-02-18 15:18:07.900031", "modified_by": "Administrator", "module": "CRM", "name": "Twitter Settings", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 1e1beab2d25..47756560ec5 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -32,7 +32,9 @@ class TwitterSettings(Document): try: auth.get_access_token(oauth_verifier) - api = self.get_api(auth.access_token, auth.access_token_secret) + self.access_token = auth.access_token + self.access_token_secret = auth.access_token_secret + api = self.get_api() user = api.me() profile_pic = (user._json["profile_image_url"]).replace("_normal","") @@ -50,11 +52,11 @@ class TwitterSettings(Document): frappe.msgprint(_("Error! Failed to get access token.")) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) - def get_api(self, access_token, access_token_secret): - # authentication of consumer key and secret - auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) - # authentication of access token and secret - auth.set_access_token(access_token, access_token_secret) + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.access_token, self.access_token_secret) return tweepy.API(auth) @@ -68,13 +70,13 @@ class TwitterSettings(Document): def upload_image(self, media): media = get_file_path(media) - api = self.get_api(self.access_token, self.access_token_secret) + api = self.get_api() media = api.media_upload(media) return media.media_id def send_tweet(self, text, media_id=None): - api = self.get_api(self.access_token, self.access_token_secret) + api = self.get_api() try: if media_id: response = api.update_status(status = text, media_ids = [media_id]) @@ -84,12 +86,32 @@ class TwitterSettings(Document): return response except TweepError as e: - content = json.loads(e.response.content) - content = content["errors"][0] - if e.response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + self.api_error(e) + + def delete_tweet(self, tweet_id): + api = self.get_api() + try: + api.destroy_status(tweet_id) + except TweepError as e: + self.api_error(e) + + def get_tweet(self, tweet_id): + api = self.get_api() + try: + response = api.get_status(tweet_id, trim_user=True, include_entities=True) + except TweepError as e: + self.api_error(e) + + return response._json + + def api_error(self, e): + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason)) + @frappe.whitelist(allow_guest=True) def callback(oauth_token = None, oauth_verifier = None): diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json index a59f149ee56..68035281561 100644 --- a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json +++ b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.008622", - "modified": "2020-07-22 13:36:48.114479", + "last_synced_on": "2021-01-30 21:03:30.086891", + "modified": "2021-02-01 13:36:04.469863", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedures", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json b/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json index 6d560f74bf1..dae9db19b8d 100644 --- a/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json +++ b/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:46.691764", - "modified": "2020-07-22 13:40:17.215775", + "last_synced_on": "2021-02-01 13:36:38.787783", + "modified": "2021-02-01 13:37:18.718275", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedures Status", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Pie", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json b/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json index 0195aac8b73..82145d60248 100644 --- a/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json +++ b/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json @@ -5,21 +5,22 @@ "docstatus": 0, "doctype": "Dashboard Chart", "document_type": "Patient Encounter Diagnosis", + "dynamic_filters_json": "", "filters_json": "[]", "group_by_based_on": "diagnosis", "group_by_type": "Count", "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.895521", - "modified": "2020-07-22 13:43:32.369481", + "last_synced_on": "2021-01-30 21:03:33.729487", + "modified": "2021-02-01 13:34:57.385335", "modified_by": "Administrator", "module": "Healthcare", "name": "Diagnoses", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json b/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json index 052483533e9..70293b158ed 100644 --- a/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json +++ b/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.344055", - "modified": "2020-07-22 13:37:34.490129", + "last_synced_on": "2021-01-30 21:03:28.272914", + "modified": "2021-02-01 13:36:08.391433", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Tests", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json b/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json index 8fc86a1c592..65e5472aa10 100644 --- a/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json +++ b/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.296748", - "modified": "2020-07-22 13:40:59.655129", + "last_synced_on": "2021-01-30 21:03:32.067473", + "modified": "2021-02-01 13:35:30.953718", "modified_by": "Administrator", "module": "Healthcare", "name": "Symptoms", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py index ff9e21252a1..0c463ddc029 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py @@ -118,12 +118,12 @@ class TestInpatientMedicationEntry(unittest.TestCase): def tearDown(self): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) for entry in frappe.get_all('Inpatient Medication Entry'): doc = frappe.get_doc('Inpatient Medication Entry', entry.name) diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py index 798976283b3..ec1a28034e3 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py +++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py @@ -40,13 +40,13 @@ class TestInpatientMedicationOrder(unittest.TestCase): def test_inpatient_validation(self): # Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) ipmo = create_ipmo(self.patient) # inpatient validation @@ -74,12 +74,12 @@ class TestInpatientMedicationOrder(unittest.TestCase): def tearDown(self): if frappe.db.get_value('Patient', self.patient, 'inpatient_record'): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient})) + schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() - discharge_patient(self.ip_record) + discharge_patient(self.ip_record, now_datetime()) for doctype in ["Inpatient Medication Entry", "Inpatient Medication Order"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js index 60f0f9d56d6..750279e348b 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js @@ -41,17 +41,36 @@ frappe.ui.form.on('Inpatient Record', { }); let discharge_patient = function(frm) { - frappe.call({ - doc: frm.doc, - method: 'discharge', - callback: function(data) { - if (!data.exc) { - frm.reload_doc(); + let dialog = new frappe.ui.Dialog({ + title: 'Discharge Patient', + width: 100, + fields: [ + {fieldtype: 'Datetime', label: 'Discharge Datetime', fieldname: 'check_out', + reqd: 1, default: frappe.datetime.now_datetime() } - }, - freeze: true, - freeze_message: __('Processing Inpatient Discharge') + ], + primary_action_label: __('Discharge'), + primary_action: function() { + let check_out = dialog.get_value('check_out'); + frappe.call({ + doc: frm.doc, + method: 'discharge', + args: { + 'check_out': check_out + }, + callback: function(data) { + if (!data.exc) { + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: __('Processing Inpatient Discharge') + }); + frm.refresh_fields(); + dialog.hide(); + } }); + dialog.show(); }; let admit_patient_dialog = function(frm) { diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index 0e1c2ba7664..03ecf4fb018 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -50,7 +50,7 @@ "inpatient_occupancies", "btn_transfer", "sb_discharge_details", - "discharge_ordered_date", + "discharge_ordered_datetime", "discharge_practitioner", "discharge_encounter", "discharge_datetime", @@ -374,13 +374,6 @@ "fieldtype": "Small Text", "label": "Discharge Instructions" }, - { - "fieldname": "discharge_ordered_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Discharge Ordered Date", - "read_only": 1 - }, { "collapsible": 1, "fieldname": "rehabilitation_section", @@ -406,13 +399,20 @@ { "fieldname": "discharge_datetime", "fieldtype": "Datetime", - "label": "Discharge Date", + "label": "Discharge Datetime", "permlevel": 2 + }, + { + "fieldname": "discharge_ordered_datetime", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Discharge Ordered Datetime", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-18 15:59:17.318988", + "modified": "2021-08-09 22:49:07.419692", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index f4d1eaf2e3f..2cdfa04d5c0 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -27,7 +27,7 @@ class InpatientRecord(Document): def validate_dates(self): if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \ - (getdate(self.discharge_ordered_date) < getdate(self.scheduled_date)): + (getdate(self.discharge_ordered_datetime) < getdate(self.scheduled_date)): frappe.throw(_('Expected and Discharge dates cannot be less than Admission Schedule date')) for entry in self.inpatient_occupancies: @@ -58,8 +58,10 @@ class InpatientRecord(Document): admit_patient(self, service_unit, check_in, expected_discharge) @frappe.whitelist() - def discharge(self): - discharge_patient(self) + def discharge(self, check_out=now_datetime()): + if (getdate(check_out) < getdate(self.admitted_datetime)): + frappe.throw(_('Discharge date cannot be less than Admission date')) + discharge_patient(self, check_out) @frappe.whitelist() def transfer(self, service_unit, check_in, leave_from): @@ -120,10 +122,13 @@ def schedule_inpatient(args): @frappe.whitelist() def schedule_discharge(args): discharge_order = json.loads(args) + if not discharge_order or not discharge_order['patient'] or not discharge_order['discharge_ordered_datetime']: + frappe.throw(_('Missing required details, did not create schedule discharge')) + inpatient_record_id = frappe.db.get_value('Patient', discharge_order['patient'], 'inpatient_record') if inpatient_record_id: inpatient_record = frappe.get_doc('Inpatient Record', inpatient_record_id) - check_out_inpatient(inpatient_record) + check_out_inpatient(inpatient_record, discharge_order['discharge_ordered_datetime']) set_details_from_ip_order(inpatient_record, discharge_order) inpatient_record.status = 'Discharge Scheduled' inpatient_record.save(ignore_permissions = True) @@ -143,18 +148,18 @@ def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_chi table.set(df.fieldname, item.get(df.fieldname)) -def check_out_inpatient(inpatient_record): +def check_out_inpatient(inpatient_record, discharge_ordered_datetime): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: if inpatient_occupancy.left != 1: inpatient_occupancy.left = True - inpatient_occupancy.check_out = now_datetime() + inpatient_occupancy.check_out = discharge_ordered_datetime frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") -def discharge_patient(inpatient_record): +def discharge_patient(inpatient_record, check_out): validate_inpatient_invoicing(inpatient_record) - inpatient_record.discharge_datetime = now_datetime() + inpatient_record.discharge_datetime = check_out inpatient_record.status = "Discharged" inpatient_record.save(ignore_permissions = True) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index b4a961264f2..9b5cd717a0c 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -29,7 +29,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) # Discharge - schedule_discharge(frappe.as_json({'patient': patient})) + schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) @@ -37,7 +37,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertRaises(frappe.ValidationError, ip_record.discharge) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) @@ -56,7 +56,7 @@ class TestInpatientRecord(unittest.TestCase): admit_patient(ip_record, service_unit, now_datetime()) # Discharge - schedule_discharge(frappe.as_json({"patient": patient})) + schedule_discharge(frappe.as_json({"patient": patient, 'discharge_ordered_datetime': now_datetime()})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record = frappe.get_doc("Inpatient Record", ip_record.name) @@ -88,12 +88,12 @@ class TestInpatientRecord(unittest.TestCase): self.assertFalse(patient_encounter.name in encounter_ids) # Discharge - schedule_discharge(frappe.as_json({"patient": patient})) + schedule_discharge(frappe.as_json({"patient": patient, 'discharge_ordered_datetime': now_datetime()})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record) - discharge_patient(ip_record) + discharge_patient(ip_record, now_datetime()) setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0) def test_validate_overlap_admission(self): diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 4b57cd073d0..74495a85910 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -34,7 +34,7 @@ class LabTest(Document): frappe.db.set_value('Lab Prescription', self.prescription, 'lab_test_created', 1) if frappe.db.get_value('Lab Prescription', self.prescription, 'invoiced'): self.invoiced = True - if not self.lab_test_name and self.template: + if self.template: self.load_test_from_template() self.reload() @@ -50,7 +50,7 @@ class LabTest(Document): item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except: item.secondary_uom_result = '' - frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated'.format(item.idx)), title = _('Warning')) + frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated').format(item.idx), title = _('Warning')) def validate_result_values(self): if self.normal_test_items: @@ -229,9 +229,9 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: - sample_details = sample_collection.sample_details + '\n-\n' + _('Test: ') + sample_details = sample_collection.sample_details + '\n-\n' + _('Test :') sample_details += (template.get('lab_test_name') or template.get('template')) + '\n' - sample_details += _('Collection Details: ') + '\n\t' + template.sample_details + sample_details += _('Collection Details:') + '\n\t' + template.sample_details frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_details', sample_details) frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_qty', quantity) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index bb5abd53ba7..18dc5bd5cea 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -62,12 +62,13 @@ class TestPatientAppointment(unittest.TestCase): def test_auto_invoicing_based_on_department(self): patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment_type = create_appointment_type() + appointment_type = create_appointment_type({'medical_department': medical_department}) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), - invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department') + invoice=1, appointment_type=appointment_type.name, department=medical_department) appointment.reload() self.assertEqual(appointment.invoiced, 1) @@ -89,9 +90,9 @@ class TestPatientAppointment(unittest.TestCase): 'op_consulting_charge': 300 }] appointment_type = create_appointment_type(args={ - 'name': 'Generic Appointment Type charge', - 'items': items - }) + 'name': 'Generic Appointment Type charge', + 'items': items + }) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1, appointment_type=appointment_type.name) @@ -146,10 +147,10 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(appointment.service_unit, service_unit) # Discharge - schedule_discharge(frappe.as_json({'patient': patient})) + schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) def test_invalid_healthcare_service_unit_validation(self): from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge @@ -173,10 +174,10 @@ class TestPatientAppointment(unittest.TestCase): self.assertRaises(frappe.exceptions.ValidationError, appointment.save) # Discharge - schedule_discharge(frappe.as_json({'patient': patient})) + schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) - discharge_patient(ip_record1) + discharge_patient(ip_record1, now_datetime()) def test_overlap_appointment(self): from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError @@ -360,9 +361,9 @@ def create_appointment_type(args=None): else: item = create_healthcare_service_items() items = [{ - 'medical_department': '_Test Medical Department', - 'op_consulting_charge_item': item, - 'op_consulting_charge': 200 + 'medical_department': args.get('medical_department') or '_Test Medical Department', + 'op_consulting_charge_item': item, + 'op_consulting_charge': 200 }] return frappe.get_doc({ 'doctype': 'Appointment Type', @@ -372,6 +373,8 @@ def create_appointment_type(args=None): 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}), 'items': args.get('items') or items }).insert() + + def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0): if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'): return f'_Test Service Unit Type {str(id)}' diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index aaeaa692e63..5b950a8809e 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -257,7 +257,7 @@ var schedule_discharge = function(frm) { var dialog = new frappe.ui.Dialog ({ title: 'Inpatient Discharge', fields: [ - {fieldtype: 'Date', label: 'Discharge Ordered Date', fieldname: 'discharge_ordered_date', default: 'Today', read_only: 1}, + {fieldtype: 'Datetime', label: 'Discharge Ordered DateTime', fieldname: 'discharge_ordered_datetime', default: frappe.datetime.now_datetime()}, {fieldtype: 'Date', label: 'Followup Date', fieldname: 'followup_date'}, {fieldtype: 'Column Break'}, {fieldtype: 'Small Text', label: 'Discharge Instructions', fieldname: 'discharge_instructions'}, @@ -270,7 +270,7 @@ var schedule_discharge = function(frm) { patient: frm.doc.patient, discharge_encounter: frm.doc.name, discharge_practitioner: frm.doc.practitioner, - discharge_ordered_date: dialog.get_value('discharge_ordered_date'), + discharge_ordered_datetime: dialog.get_value('discharge_ordered_datetime'), followup_date: dialog.get_value('followup_date'), discharge_instructions: dialog.get_value('discharge_instructions'), discharge_note: dialog.get_value('discharge_note') 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 63b00859d71..9e0d3c3e278 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -18,7 +18,7 @@ class PatientHistorySettings(Document): def validate_submittable_doctypes(self): for entry in self.custom_doctypes: if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')): - msg = _('Row #{0}: Document Type {1} is not submittable. ').format( + msg = _('Row #{0}: Document Type {1} is not submittable.').format( entry.idx, frappe.bold(entry.document_type)) msg += _('Patient Medical Record can only be created for submittable document types.') frappe.throw(msg) @@ -116,12 +116,12 @@ def set_subject_field(doc): fieldname = entry.get('fieldname') 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) + '
' + 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 += frappe.bold(_(entry.get('label')) + ':') + cstr(formatted_value) + '
' return subject 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 index f523cd5edea..9169ea642b9 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest import json -from frappe.utils import getdate +from frappe.utils import getdate, strip_html from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient class TestPatientHistorySettings(unittest.TestCase): @@ -38,15 +38,14 @@ class TestPatientHistorySettings(unittest.TestCase): # tests for medical record creation of standard doctypes in test_patient_medical_record.py patient = create_patient() doc = create_doc(patient) - # check for medical record medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name}) self.assertTrue(medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) - expected_subject = "Date: {0}
Rating: 3
Feedback: Test Patient History Settings
".format( + expected_subject = "Date:{0}Rating:3Feedback:Test Patient History Settings".format( frappe.utils.format_date(getdate())) - self.assertEqual(medical_rec.subject, expected_subject) + self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.communication_date, getdate()) diff --git a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json index 56c3c135599..0aa8f9a027e 100644 --- a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json +++ b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json @@ -10,7 +10,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/healthcare", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:06:19.512946", + "modified": "2021-01-30 19:22:20.273766", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json index c45a347080c..3f25a9d6760 100644 --- a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json +++ b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 23:16:31.965521", + "modified": "2021-01-30 12:02:22.849260", "modified_by": "Administrator", "name": "Create Healthcare Practitioner", "owner": "Administrator", "reference_document": "Healthcare Practitioner", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Healthcare Practitioner", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json index 77bc5bd7adf..b46bb15b48a 100644 --- a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json +++ b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-19 12:26:24.023418", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.786428", + "modified_by": "ruchamahabal2@gmail.com", "name": "Create Patient", "owner": "Administrator", "reference_document": "Patient", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Patient", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json index 65980ef6687..7ce122d5c0b 100644 --- a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json +++ b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-19 12:27:09.437825", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.794602", + "modified_by": "ruchamahabal2@gmail.com", "name": "Create Practitioner Schedule", "owner": "Administrator", "reference_document": "Practitioner Schedule", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Practitioner Schedule", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json index 697b761e528..dfe9f71a76f 100644 --- a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json +++ b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 23:10:24.504030", + "modified": "2021-01-30 19:22:08.257160", "modified_by": "Administrator", "name": "Explore Clinical Procedure Templates", "owner": "Administrator", "reference_document": "Clinical Procedure Template", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Clinical Procedure Templates", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json index b2d5aef4312..2d952f30938 100644 --- a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json +++ b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 1, "is_skipped": 0, - "modified": "2020-05-26 23:10:24.507648", + "modified": "2021-01-30 19:22:07.275735", "modified_by": "Administrator", "name": "Explore Healthcare Settings", "owner": "Administrator", "reference_document": "Healthcare Settings", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Healthcare Settings", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json index fa4c9036d7e..baa8358c060 100644 --- a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json +++ b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json @@ -6,14 +6,14 @@ "field": "schedule", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 22:07:07.482530", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.807129", + "modified_by": "ruchamahabal2@gmail.com", "name": "Introduction to Healthcare Practitioner", "owner": "Administrator", "reference_document": "Healthcare Practitioner", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Healthcare Practitioner", "validate_action": 0 diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css index 1bb589164e6..74b5e7eb918 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.css +++ b/erpnext/healthcare/page/patient_history/patient_history.css @@ -9,6 +9,26 @@ cursor: pointer; } +.patient-image-container { + margin-top: 17px; + } + +.patient-image { + display: inline-block; + width: 100%; + height: 0; + padding: 50% 0px; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: 4px; +} + +.patient-name { + font-size: 20px; + margin-top: 25px; +} + .medical_record-label { max-width: 100px; margin-bottom: -4px; @@ -19,19 +39,19 @@ } .date-indicator { - background:none; - font-size:12px; - vertical-align:middle; - font-weight:bold; - color:#6c7680; + background:none; + font-size:12px; + vertical-align:middle; + font-weight:bold; + color:#6c7680; } .date-indicator::after { - margin:0 -4px 0 12px; - content:''; - display:inline-block; - height:8px; - width:8px; - border-radius:8px; + margin:0 -4px 0 12px; + content:''; + display:inline-block; + height:8px; + width:8px; + border-radius:8px; background: #d1d8dd; } diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index f1706557f45..d16b38637cd 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -1,26 +1,18 @@ -
-
-

-
+
+
+
+
+
+
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
+ +
+
+
+
+
+
+
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 54343aae449..bf947cac215 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -1,403 +1,455 @@ frappe.provide('frappe.patient_history'); frappe.pages['patient_history'].on_page_load = function(wrapper) { - let me = this; - let page = frappe.ui.make_app_page({ + frappe.ui.make_app_page({ parent: wrapper, - title: 'Patient History', - single_column: true + title: __('Patient History') }); - frappe.breadcrumbs.add('Healthcare'); - let pid = ''; - page.main.html(frappe.render_template('patient_history', {})); - page.main.find('.header-separator').hide(); - - let patient = frappe.ui.form.make_control({ - parent: page.main.find('.patient'), - df: { - fieldtype: 'Link', - options: 'Patient', - fieldname: 'patient', - placeholder: __('Select Patient'), - only_select: true, - change: function() { - let patient_id = patient.get_value(); - if (pid != patient_id && patient_id) { - me.start = 0; - me.page.main.find('.patient_documents_list').html(''); - 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_id; - } - }, - }); - patient.refresh(); - - if (frappe.route_options) { - patient.set_value(frappe.route_options.patient); - } - - this.page.main.on('click', '.btn-show-chart', function() { - let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts'); - let title = $(this).attr('data-title'); - show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title); - }); - - this.page.main.on('click', '.btn-more', function() { - let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); - if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { - me.page.main.find('.'+docname).hide(); - me.page.main.find('.'+docname).parent().find('.document-html').show(); - } else { - if (doctype && docname) { - let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date']; - frappe.call({ - method: 'erpnext.healthcare.utils.render_doc_as_html', - args:{ - doctype: doctype, - docname: docname, - exclude_fields: exclude - }, - freeze: true, - callback: function(r) { - 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'); - } - } - }); - } - } - }); - - 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() { - get_documents(patient.get_value(), me); + let patient_history = new PatientHistory(wrapper); + $(wrapper).bind('show', ()=> { + patient_history.show(); }); }; -let setup_filters = function(patient, me) { - $('.doctype-filter').empty(); - 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'), +class PatientHistory { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + this.sidebar = this.wrapper.find('.layout-side-section'); + this.main_section = this.wrapper.find('.layout-main-section'); + this.start = 0; + } + + show() { + frappe.breadcrumbs.add('Healthcare'); + this.sidebar.empty(); + + let me = this; + let patient = frappe.ui.form.make_control({ + parent: me.sidebar, df: { - fieldtype: 'MultiSelectList', - fieldname: 'document_type', - placeholder: __('Select Document Type'), - input_class: 'input-xs', + fieldtype: 'Link', + options: 'Patient', + fieldname: 'patient', + placeholder: __('Select Patient'), + only_select: true, change: () => { - me.start = 0; - me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value()); - }, - get_data: () => { - return document_types.map(document_type => { - return { - description: document_type, - value: document_type - }; - }); - }, + me.patient_id = ''; + if (me.patient_id != patient.get_value() && patient.get_value()) { + me.start = 0; + me.patient_id = patient.get_value(); + me.make_patient_profile(); + } + } } }); - doctype_filter.refresh(); + patient.refresh(); - $('.date-filter').empty(); - 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) { + if (frappe.route_options && !this.patient_id) { + patient.set_value(frappe.route_options.patient); + this.patient_id = frappe.route_options.patient; + } + + this.sidebar.find('[data-fieldname="patient"]').append('
'); + } + + make_patient_profile() { + this.page.set_title(__('Patient History')); + this.main_section.empty().append(frappe.render_template('patient_history')); + this.setup_filters(); + this.setup_documents(); + this.show_patient_info(); + this.setup_buttons(); + this.show_patient_vital_charts('bp', 'mmHg', 'Blood Pressure'); + } + + setup_filters() { + $('.doctype-filter').empty(); + let me = this; + + 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'), + change: () => { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value(), selected_date_range); - } + this.setup_documents(doctype_filter.get_value(), date_range_field.get_value()); + }, + get_data: () => { + return document_types.map(document_type => { + return { + description: document_type, + value: document_type + }; + }); + }, } - }, - parent: $('.date-filter') + }); + doctype_filter.refresh(); + + $('.date-filter').empty(); + 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(''); + this.setup_documents(doctype_filter.get_value(), date_range_field.get_value()); + } + } + }, + parent: $('.date-filter') + }); + date_range_field.refresh(); }); - date_range_field.refresh(); - }); -}; + } -let get_documents = function(patient, me, document_types="", selected_date_range="") { - let filters = { - name: patient, - start: me.start, - page_length: 20 - }; - if (document_types) - filters['document_types'] = document_types; - if (selected_date_range) - filters['date_range'] = selected_date_range; + setup_documents(document_types="", selected_date_range="") { + let filters = { + name: this.patient_id, + start: this.start, + page_length: 20 + }; + 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', - args: filters, - 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(); + let me = this; + frappe.call({ + 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', + args: filters, + callback: function(r) { + let data = r.message; + if (data.length) { + me.add_to_records(data); + } else { + me.page.main.find('.patient_documents_list').append(` +
+

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

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

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

+
`); } } - details += ''; - me.page.main.find('.patient_documents_list').append(details); - me.start += data.length; + add_date_separator(data) { + let date = frappe.datetime.str_to_obj(data.communication_date); + let pdate = ''; + let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), + frappe.datetime.obj_to_str(date)); - if (data.length === 20) { - me.page.main.find(".btn-get-records").show(); - } else { - me.page.main.find(".btn-get-records").hide(); - me.page.main.find(".patient_documents_list").append(` -
-

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

-
`); - } -}; - -let add_date_separator = function(data) { - let date = frappe.datetime.str_to_obj(data.communication_date); - let pdate = ''; - let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - - if (diff < 1) { - pdate = __('Today'); - } else if (diff < 2) { - pdate = __('Yesterday'); - } else { - pdate = __('on ') + frappe.datetime.global_date_format(date); - } - data.date_sep = pdate; - return data; -}; - -let show_patient_info = function(patient, me) { - frappe.call({ - 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', - args: { - patient: patient - }, - 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 += `

${__('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); + if (diff < 1) { + pdate = __('Today'); + } else if (diff < 2) { + pdate = __('Yesterday'); + } else { + pdate = __('on {0}', [frappe.datetime.global_date_format(date)]); } - }); -}; + data.date_sep = pdate; + return data; + } -let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { - frappe.call({ - method: 'erpnext.healthcare.utils.get_patient_vitals', - args:{ - patient: patient - }, - callback: function(r) { - if (r.message) { - let show_chart_btns_html = ` -
- - ${__('Blood Pressure')} - - - ${__('Respiratory/Pulse Rate')} - - - ${__('Temperature')} - - - ${__('BMI')} - -
`; + show_patient_info() { + this.get_patient_info().then(() => { + $('.patient-info').empty().append(frappe.render_template('patient_history_sidebar', { + patient_image: this.patient.image, + patient_name: this.patient.patient_name, + patient_gender: this.patient.sex, + patient_mobile: this.patient.mobile + })); + this.show_patient_details(); + }); + } - me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + show_patient_details() { + let me = this; + frappe.call({ + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', + args: { + patient: me.patient_id + }, + callback: function(r) { let data = r.message; - let labels = [], datasets = []; - let bp_systolic = [], bp_diastolic = [], temperature = []; - let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; + let details = ``; - for (let i=0; i
${__('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 (btn_show_id === 'bp') { - bp_systolic.push(data[i].bp_systolic); - bp_diastolic.push(data[i].bp_diastolic); - } - if (btn_show_id === 'temperature') { - temperature.push(data[i].temperature); - } - if (btn_show_id === 'pulse_rate') { - pulse.push(data[i].pulse); - respiratory_rate.push(data[i].respiratory_rate); - } - if (btn_show_id === 'bmi') { - bmi.push(data[i].bmi); - height.push(data[i].height); - weight.push(data[i].weight); - } + if (details) { + details = `
` + details + `
`; } - if (btn_show_id === 'temperature') { - datasets.push({name: 'Temperature', values: temperature, chartType: 'line'}); - } - if (btn_show_id === 'bmi') { - datasets.push({name: 'BMI', values: bmi, chartType: 'line'}); - datasets.push({name: 'Height', values: height, chartType: 'line'}); - datasets.push({name: 'Weight', values: weight, chartType: 'line'}); - } - if (btn_show_id === 'bp') { - datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'}); - datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'}); - } - if (btn_show_id === 'pulse_rate') { - datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'}); - datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'}); - } - new frappe.Chart('.patient_vital_charts', { - data: { - labels: labels, - datasets: datasets - }, - title: title, - type: 'axis-mixed', - height: 200, - colors: ['purple', '#ffa3ef', 'light-blue'], - - tooltipOptions: { - formatTooltipX: d => (d + '').toUpperCase(), - formatTooltipY: d => d + ' ' + pts, - } - }); - me.page.main.find('.header-separator').show(); - } else { - me.page.main.find('.patient_vital_charts').html(''); - me.page.main.find('.show_chart_btns').html(''); - me.page.main.find('.header-separator').hide(); + me.sidebar.find('.patient-details').html(details); } - } - }); -}; + }); + } + + get_patient_info() { + return frappe.xcall('frappe.client.get', { + doctype: 'Patient', + name: this.patient_id, + }).then((patient) => { + if (patient) { + this.patient = patient; + } + }); + } + + setup_buttons() { + let me = this; + this.page.main.on("click", ".btn-show-chart", function() { + let btn_id = $(this).attr("data-show-chart-id"), scale_unit = $(this).attr("data-pts"); + let title = $(this).attr("data-title"); + me.show_patient_vital_charts(btn_id, scale_unit, title); + }); + + this.page.main.on('click', '.btn-more', function() { + let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); + if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { + me.page.main.find('.'+docname).hide(); + me.page.main.find('.'+docname).parent().find('.document-html').show(); + } else { + if (doctype && docname) { + let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date', 'naming_series']; + frappe.call({ + method: 'erpnext.healthcare.utils.render_doc_as_html', + args: { + doctype: doctype, + docname: docname, + exclude_fields: exclude + }, + freeze: true, + callback: function(r) { + 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').attr('hidden', false); + me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); + } + } + }); + } + } + }); + + 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.page.main.on('click', '.btn-get-records', function() { + this.setup_documents(); + }); + } + + show_patient_vital_charts(btn_id, scale_unit, title) { + let me = this; + + frappe.call({ + method: 'erpnext.healthcare.utils.get_patient_vitals', + args: { + patient: me.patient_id + }, + callback: function(r) { + 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 (let i=0; i (d + '').toUpperCase(), + formatTooltipY: d => d + ' ' + scale_unit, + } + }); + me.page.main.find('.header-separator').show(); + } else { + me.page.main.find('.patient_vital_charts').html(''); + me.page.main.find('.show_chart_btns').html(''); + me.page.main.find('.header-separator').hide(); + } + } + }); + } +} \ No newline at end of file diff --git a/erpnext/healthcare/page/patient_history/patient_history_sidebar.html b/erpnext/healthcare/page/patient_history/patient_history_sidebar.html new file mode 100644 index 00000000000..4560e7e1254 --- /dev/null +++ b/erpnext/healthcare/page/patient_history/patient_history_sidebar.html @@ -0,0 +1,21 @@ +
+
+ {% if patient_image %} +
+ {% endif %} +
+
+ {% if patient_name %} +

{{patient_name}}

+ {% endif %} + {% if patient_gender %} +

{%=__("Gender: ") %} {{patient_gender}}

+ {% endif %} + {% if patient_mobile %} +

{%=__("Contact: ") %} {{patient_mobile}}

+ {% endif %} +
+
+
+
+ diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.css b/erpnext/healthcare/page/patient_progress/patient_progress.css index 5d85a7487fd..737b2e0ea28 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.css +++ b/erpnext/healthcare/page/patient_progress/patient_progress.css @@ -29,6 +29,7 @@ .patient-name { font-size: 20px; + margin-top: 25px; } /* heatmap */ @@ -55,6 +56,7 @@ } .heatmap-container .chart-filter { + z-index: 1; position: relative; top: 5px; margin-right: 10px; @@ -111,10 +113,13 @@ text.title { } .chart-column-container { - border-bottom: 1px solid #d1d8dd; margin: 5px 0; } +.progress-graphs .progress-container { + margin-bottom: var(--margin-xl); +} + .line-chart-container .frappe-chart { margin-top: -20px; } @@ -146,6 +151,7 @@ text.title { } .percentage-chart-container .chart-filter { + z-index: 1; position: relative; top: 12px; margin-right: 10px; diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.html b/erpnext/healthcare/page/patient_progress/patient_progress.html index 30064bd1654..ee60065618f 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.html +++ b/erpnext/healthcare/page/patient_progress/patient_progress.html @@ -1,14 +1,15 @@
-