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 = "
";
- let i;
- for (i=0; i" + data[i].subject;
- }
- data[i] = add_date_separator(data[i]);
+ add_to_records(data) {
+ let details = "";
+ let i;
+ for (i=0; i" + data[i].subject;
+ }
+ data[i] = this.add_date_separator(data[i]);
- if (frappe.user_info(data[i].owner).image) {
- data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image);
- } else {
- data[i].imgsrc = false;
- }
+ if (frappe.user_info(data[i].owner).image) {
+ data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image);
+ } else {
+ data[i].imgsrc = false;
+ }
- let time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
- time_line_heading += data[i].reference_doctype + " - " +
- `
- ${data[i].reference_name}
- `;
+ let time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
+ time_line_heading += data[i].reference_doctype + " - " +
+ `
+ ${data[i].reference_name}
+ `;
- details += `
-