Merge branch 'develop' of github.com:frappe/erpnext into version-13-beta-pre-release

This commit is contained in:
Suraj Shetty 2021-01-25 16:06:38 +05:30
commit 0c34a711eb
269 changed files with 7637 additions and 2848 deletions

View File

@ -21,8 +21,8 @@ def docs_link_exists(body):
if word.startswith('http') and uri_validator(word): if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word) parsed_url = urlparse(word)
if parsed_url.netloc == "github.com": if parsed_url.netloc == "github.com":
_, org, repo, _type, ref = parsed_url.path.split('/') parts = parsed_url.path.split('/')
if org == "frappe" and repo in docs_repos: if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True return True

View File

@ -132,16 +132,10 @@ def allow_regional(fn):
return caller return caller
def get_last_membership(): def get_last_membership(member):
'''Returns last membership if exists''' '''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) dict(member=member, paid=1), order_by='to_date desc', limit=1)
return last_membership and last_membership[0] if last_membership:
return last_membership[0]
def is_member():
'''Returns true if the user is still a member'''
last_membership = get_last_membership()
if last_membership and getdate(last_membership.to_date) > getdate():
return True
return False

View File

@ -910,98 +910,8 @@
}, },
"is_group": 1 "is_group": 1
}, },
"Passiva": { "Passiva - Verbindlichkeiten": {
"root_type": "Liability", "root_type": "Liability",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_type": "Equity",
"account_number": "2900"
},
"Ausstehende Einlagen auf das gezeichnete Kapital": {
"account_number": "2910",
"is_group": 1
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
},
"B - R\u00fcckstellungen": { "B - R\u00fcckstellungen": {
"is_group": 1, "is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": { "1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@ -1618,6 +1528,143 @@
}, },
"is_group": 1 "is_group": 1
}, },
"Passiva - Eigenkapital": {
"root_type": "Equity",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_number": "2900",
"account_type": "Equity"
},
"Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
"account_number": "2901"
},
"Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
"account_number": "2902"
},
"Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
"account_number": "2903"
},
"R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2906"
},
"Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2907"
},
"Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
"account_number": "2908"
},
"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
"account_number": "2910"
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
},
"Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
"account_number": "2925"
},
"Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
"account_number": "2926"
},
"Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
"account_number": "2927"
},
"Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
"account_number": "2928"
},
"Nachschusskapital (Gegenkonto 1299)": {
"account_number": "2929"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1,
"R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_number": "2935"
}
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Andere Gewinnr\u00fccklagen": {
"account_number": "2960"
},
"Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
"account_number": "2961"
},
"Eigenkapitalanteil von Wertaufholungen": {
"account_number": "2962"
},
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
}
},
"1 - Umsatzerl\u00f6se": { "1 - Umsatzerl\u00f6se": {
"root_type": "Income", "root_type": "Income",
"is_group": 1, "is_group": 1,

View File

@ -2,7 +2,6 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension', { frappe.ui.form.on('Accounting Dimension', {
refresh: function(frm) { refresh: function(frm) {
frm.set_query('document_type', () => { frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list; let invalid_doctypes = frappe.model.core_doctypes_list;

View File

@ -203,7 +203,7 @@ def get_dimension_with_children(doctype, dimension):
return all_dimensions return all_dimensions
@frappe.whitelist() @frappe.whitelist()
def get_dimension_filters(): def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql(""" dimension_filters = frappe.db.sql("""
SELECT label, fieldname, document_type SELECT label, fieldname, document_type
FROM `tabAccounting Dimension` FROM `tabAccounting Dimension`
@ -214,6 +214,18 @@ def get_dimension_filters():
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1) WHERE c.parent = p.name""", as_dict=1)
if with_cost_center_and_project:
dimension_filters.extend([
{
'fieldname': 'cost_center',
'document_type': 'Cost Center'
},
{
'fieldname': 'project',
'document_type': 'Project'
}
])
default_dimensions_map = {} default_dimensions_map = {}
for dimension in default_dimensions: for dimension in default_dimensions:
default_dimensions_map.setdefault(dimension.company, {}) default_dimensions_map.setdefault(dimension.company, {})

View File

@ -11,37 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import d
class TestAccountingDimension(unittest.TestCase): class TestAccountingDimension(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.set_user("Administrator") create_dimension()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
dimension = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Department",
}).insert()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 0
dimension1.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Location",
})
dimension1.append("dimension_defaults", {
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1
})
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def test_dimension_against_sales_invoice(self): def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
@ -101,6 +71,38 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self): def tearDown(self):
disable_dimension() disable_dimension()
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Department",
}).insert()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Location",
})
dimension1.append("dimension_defaults", {
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1
})
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def disable_dimension(): def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department") dimension1 = frappe.get_doc("Accounting Dimension", "Department")

View File

@ -0,0 +1,82 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension Filter', {
refresh: function(frm, cdt, cdn) {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
let help_content =
`<table class="table table-bordered" style="background-color: #f9f9f9;">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
{{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
</p>
</td></tr>
</table>`;
frm.set_df_property('dimension_filter_help', 'options', help_content);
},
onload: function(frm) {
frm.set_query('applicable_on_account', 'accounts', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
frappe.db.get_list('Accounting Dimension',
{fields: ['document_type']}).then((res) => {
let options = ['Cost Center', 'Project'];
res.forEach((dimension) => {
options.push(dimension.document_type);
});
frm.set_df_property('accounting_dimension', 'options', options);
});
frm.trigger('setup_filters');
},
setup_filters: function(frm) {
let filters = {};
if (frm.doc.accounting_dimension) {
frappe.model.with_doctype(frm.doc.accounting_dimension, function() {
if (frappe.model.is_tree(frm.doc.accounting_dimension)) {
filters['is_group'] = 0;
}
if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) {
filters['company'] = frm.doc.company;
}
frm.set_query('dimension_value', 'dimensions', function() {
return {
filters: filters
};
});
});
}
},
accounting_dimension: function(frm) {
frm.clear_table("dimensions");
let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension;
frm.refresh_field("dimensions");
frm.trigger('setup_filters');
},
});
frappe.ui.form.on('Allowed Dimension', {
dimensions_add: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
row.accounting_dimension = frm.doc.accounting_dimension;
frm.refresh_field("dimensions");
}
});

View File

@ -0,0 +1,134 @@
{
"actions": [],
"autoname": "format:{accounting_dimension}-{#####}",
"creation": "2020-11-08 18:28:11.906146",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"disabled",
"column_break_2",
"company",
"allow_or_restrict",
"section_break_4",
"accounts",
"column_break_6",
"dimensions",
"section_break_10",
"dimension_filter_help"
],
"fields": [
{
"fieldname": "accounting_dimension",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Accounting Dimension",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hide_border": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "allow_or_restrict",
"fieldtype": "Select",
"label": "Allow Or Restrict Dimension",
"options": "Allow\nRestrict",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Applicable On Account",
"options": "Applicable On Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.accounting_dimension",
"fieldname": "dimensions",
"fieldtype": "Table",
"label": "Applicable Dimension",
"options": "Allowed Dimension",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "dimension_filter_help",
"fieldtype": "HTML",
"label": "Dimension Filter Help",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-16 15:27:23.659285",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright, (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _, scrub
from frappe.model.document import Document
class AccountingDimensionFilter(Document):
def validate(self):
self.validate_applicable_accounts()
def validate_applicable_accounts(self):
accounts = frappe.db.sql(
"""
SELECT a.applicable_on_account as account
FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` d
WHERE d.name = a.parent
and d.name != %s
and d.accounting_dimension = %s
""", (self.name, self.accounting_dimension), as_dict=1)
account_list = [d.account for d in accounts]
for account in self.get('accounts'):
if account.applicable_on_account in account_list:
frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
def get_dimension_filter_map():
filters = frappe.db.sql("""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
FROM
`tabApplicable On Account` a, `tabAllowed Dimension` d,
`tabAccounting Dimension Filter` p
WHERE
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
""", as_dict=1)
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
f.allow_or_restrict, f.is_mandatory)
return dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
map_object.setdefault((dimension, account), {
'allowed_dimensions': [],
'is_mandatory': is_mandatory,
'allow_or_restrict': allow_or_restrict
})
map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)

View File

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
create_dimension()
create_accounting_dimension_filter()
self.invoice_list = []
def test_allowed_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
si.items[0].cost_center = 'Main - _TC'
si.department = 'Accounts - _TC'
si.location = 'Block 1'
si.save()
self.assertRaises(InvalidAccountDimensionError, si.submit)
self.invoice_list.append(si)
def test_mandatory_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
si.department = ''
si.location = 'Block 1'
# Test with no department for Sales Account
si.items[0].department = ''
si.items[0].cost_center = '_Test Cost Center 2 - _TC'
si.save()
self.assertRaises(MandatoryAccountDimensionError, si.submit)
self.invoice_list.append(si)
def tearDown(self):
disable_dimension_filter()
disable_dimension()
for si in self.invoice_list:
si.load_from_db()
if si.docstatus == 1:
si.cancel()
def create_accounting_dimension_filter():
if not frappe.db.get_value('Accounting Dimension Filter',
{'accounting_dimension': 'Cost Center'}):
frappe.get_doc({
'doctype': 'Accounting Dimension Filter',
'accounting_dimension': 'Cost Center',
'allow_or_restrict': 'Allow',
'company': '_Test Company',
'accounts': [{
'applicable_on_account': 'Sales - _TC',
}],
'dimensions': [{
'accounting_dimension': 'Cost Center',
'dimension_value': '_Test Cost Center 2 - _TC'
}]
}).insert()
else:
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
doc.disabled = 0
doc.save()
if not frappe.db.get_value('Accounting Dimension Filter',
{'accounting_dimension': 'Department'}):
frappe.get_doc({
'doctype': 'Accounting Dimension Filter',
'accounting_dimension': 'Department',
'allow_or_restrict': 'Allow',
'company': '_Test Company',
'accounts': [{
'applicable_on_account': 'Sales - _TC',
'is_mandatory': 1
}],
'dimensions': [{
'accounting_dimension': 'Department',
'dimension_value': 'Accounts - _TC'
}]
}).insert()
else:
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
doc.disabled = 0
doc.save()
def disable_dimension_filter():
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
doc.disabled = 1
doc.save()
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
doc.disabled = 1
doc.save()

View File

@ -0,0 +1,43 @@
{
"actions": [],
"creation": "2020-11-08 18:22:36.001131",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"dimension_value"
],
"fields": [
{
"fieldname": "accounting_dimension",
"fieldtype": "Link",
"label": "Accounting Dimension",
"options": "DocType",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "dimension_value",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"options": "accounting_dimension",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-23 09:56:19.744200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Allowed Dimension",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class AllowedDimension(Document):
pass

View File

@ -0,0 +1,46 @@
{
"actions": [],
"creation": "2020-11-08 18:20:00.944449",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicable_on_account",
"is_mandatory"
],
"fields": [
{
"fieldname": "applicable_on_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Accounts",
"options": "Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"columns": 2,
"default": "0",
"fieldname": "is_mandatory",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-22 19:55:13.324136",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Applicable On Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class ApplicableOnAccount(Document):
pass

View File

@ -1,5 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', { frappe.ui.form.on('Bank', {
onload: function(frm) { onload: function(frm) {
@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', {
frm.set_df_property('address_and_contact', 'hidden', 0); frm.set_df_property('address_and_contact', 'hidden', 0);
frappe.contacts.render_address_and_contact(frm); frappe.contacts.render_address_and_contact(frm);
} }
}, if (frm.doc.plaid_access_token) {
frm.add_custom_button(__('Refresh Plaid Link'), () => {
new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
});
}
}
}); });
@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) {
frm.doc.name).options = options; frm.doc.name).options = options;
frm.fields_dict.bank_transaction_mapping.grid.refresh(); frm.fields_dict.bank_transaction_mapping.grid.refresh();
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
constructor(access_token) {
this.access_token = access_token;
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config();
}
async init_config() {
this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env');
this.token = await this.get_link_token_for_update();
this.init_plaid();
}
async get_link_token_for_update() {
const token = frappe.xcall(
'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update',
{ access_token: this.access_token }
)
if (!token) {
frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information'));
}
return token;
}
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
.then(() => {
me.onScriptLoaded(me);
})
.then(() => {
if (me.linkHandler) {
me.linkHandler.open();
}
})
.catch((error) => {
me.onScriptError(error);
});
}
loadScript(src) {
return new Promise(function (resolve, reject) {
if (document.querySelector("script[src='" + src + "']")) {
resolve();
return;
}
const el = document.createElement('script');
el.type = 'text/javascript';
el.async = true;
el.src = src;
el.addEventListener('load', resolve);
el.addEventListener('error', reject);
el.addEventListener('abort', reject);
document.head.appendChild(el);
});
}
onScriptLoaded(me) {
me.linkHandler = Plaid.create({
env: me.plaid_env,
token: me.token,
onSuccess: me.plaid_success
});
}
onScriptError(error) {
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
console.log(error);
}
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
}
};

View File

@ -1,24 +1,9 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Budget', { frappe.ui.form.on('Budget', {
onload: function(frm) { onload: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("project", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("account", "accounts", function() { frm.set_query("account", "accounts", function() {
return { return {
filters: { filters: {
@ -26,16 +11,18 @@ frappe.ui.form.on('Budget', {
report_type: "Profit and Loss", report_type: "Profit and Loss",
is_group: 0 is_group: 0
} }
} };
}) });
frm.set_query("monthly_distribution", function() { frm.set_query("monthly_distribution", function() {
return { return {
filters: { filters: {
fiscal_year: frm.doc.fiscal_year fiscal_year: frm.doc.fiscal_year
} }
} };
}) });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -122,8 +122,10 @@ class TestBudget(unittest.TestCase):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -147,8 +149,11 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@ -159,10 +164,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@ -181,12 +186,14 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
project=project)
self.assertTrue(frappe.db.get_value("GL Entry", self.assertTrue(frappe.db.get_value("GL Entry",
{"voucher_type": "Journal Entry", "voucher_no": jv.name})) {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
@ -289,7 +296,7 @@ def make_budget(**args):
budget = frappe.new_doc("Budget") budget = frappe.new_doc("Budget")
if budget_against == "Project": if budget_against == "Project":
budget.project = "_Test Project" budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else: else:
budget.cost_center =cost_center or "_Test Cost Center - _TC" budget.cost_center =cost_center or "_Test Cost Center - _TC"

View File

@ -11,8 +11,10 @@ from frappe.model.meta import get_field_precision
from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
from six import iteritems
exclude_from_linked_with = True exclude_from_linked_with = True
class GLEntry(Document): class GLEntry(Document):
@ -39,6 +41,7 @@ class GLEntry(Document):
if not from_repost: if not from_repost:
self.validate_account_details(adv_adj) self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs() self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_frozen_account(self.account, adv_adj) validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj) validate_balance_type(self.account, adv_adj)
@ -76,11 +79,9 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
def validate_dimensions_for_pl_and_bs(self): def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type") account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts(): for dimension in get_checks_for_pl_and_bs_accounts():
if account_type == "Profit and Loss" \ if account_type == "Profit and Loss" \
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
if not self.get(dimension.fieldname): if not self.get(dimension.fieldname):
@ -93,6 +94,25 @@ class GLEntry(Document):
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.") frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
.format(dimension.label, self.account)) .format(dimension.label, self.account))
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in iteritems(dimension_filter_map):
dimension = key[0]
account = key[1]
if self.account == account:
if value['is_mandatory'] and not self.get(dimension):
frappe.throw(_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
if value['allow_or_restrict'] == 'Allow':
if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
else:
if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \
@ -137,9 +157,10 @@ class GLEntry(Document):
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group(): if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""") and self.cost_center and _check_is_group():
.format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self): def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party) validate_party_frozen_disabled(self.party_type, self.party)

View File

@ -20,7 +20,8 @@ def get_data():
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
}, },
{ {
'items': ['Item'] 'label': _('Stock'),
'items': ['Item Groups', 'Item']
} }
] ]
} }

View File

@ -120,6 +120,8 @@ frappe.ui.form.on("Journal Entry", {
} }
} }
}); });
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
voucher_type: function(frm){ voucher_type: function(frm){
@ -197,6 +199,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
this.load_defaults(); this.load_defaults();
this.setup_queries(); this.setup_queries();
this.setup_balance_formatter(); this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
onload_post_render: function() { onload_post_render: function() {
@ -222,15 +225,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
return erpnext.journal_entry.account_query(me.frm); return erpnext.journal_entry.account_query(me.frm);
}); });
me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) {
return {
filters: {
company: me.frm.doc.company,
is_group: 0
}
};
});
me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) { me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@ -406,6 +400,8 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
} }
} }
cur_frm.cscript.update_totals(doc); cur_frm.cscript.update_totals(doc);
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts');
}, },
}); });

View File

@ -160,7 +160,7 @@ class TestJournalEntry(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_reverse_journal_entry(self): def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False) "Sales - _TC", 100, exchange_rate=50, save=False)
@ -299,15 +299,20 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_project(self): def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
project = make_project({
'project_name': 'Journal Entry Project', if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
'project_template_name': 'Test Project Template', project = make_project({
'start_date': '2020-01-01' 'project_name': 'Journal Entry Project',
}) 'project_template_name': 'Test Project Template',
'start_date': '2020-01-01'
})
project_name = project.name
else:
project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
for d in jv.accounts: for d in jv.accounts:
d.project = project.project_name d.project = project_name
jv.voucher_type = "Bank Entry" jv.voucher_type = "Bank Entry"
jv.multi_currency = 0 jv.multi_currency = 0
jv.cheque_no = "112233" jv.cheque_no = "112233"
@ -317,10 +322,10 @@ class TestJournalEntry(unittest.TestCase):
expected_values = { expected_values = {
"_Test Cash - _TC": { "_Test Cash - _TC": {
"project": project.project_name "project": project_name
}, },
"_Test Bank - _TC": { "_Test Bank - _TC": {
"project": project.project_name "project": project_name
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Loyalty Program', { frappe.ui.form.on('Loyalty Program', {
setup: function(frm) { setup: function(frm) {
var help_content = var help_content =
@ -46,20 +48,17 @@ frappe.ui.form.on('Loyalty Program', {
}; };
}); });
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
};
});
frm.set_value("company", frappe.defaults.get_user_default("Company")); frm.set_value("company", frappe.defaults.get_user_default("Company"));
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) {
frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules.")); frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules."));
} }
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
} }
}); });

View File

@ -36,6 +36,8 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
frm.page.set_indicator(__('In Progress'), 'orange'); frm.page.set_indicator(__('In Progress'), 'orange');
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
@ -100,6 +102,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
} }
}) })
} }
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
invoice_type: function(frm) { invoice_type: function(frm) {

View File

@ -1,6 +1,7 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
{% include "erpnext/public/js/controllers/accounts.js" %} {% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
@ -8,6 +9,8 @@ frappe.ui.form.on('Payment Entry', {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
} }
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
setup: function(frm) { setup: function(frm) {
@ -88,15 +91,6 @@ frappe.ui.form.on('Payment Entry', {
} }
}); });
frm.set_query("cost_center", "deductions", function() {
return {
filters: {
"is_group": 0,
"company": frm.doc.company
}
}
});
frm.set_query("reference_doctype", "references", function() { frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") { if (frm.doc.party_type=="Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
@ -167,6 +161,7 @@ frappe.ui.form.on('Payment Entry', {
company: function(frm) { company: function(frm) {
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
contact_person: function(frm) { contact_person: function(frm) {
@ -401,6 +396,8 @@ frappe.ui.form.on('Payment Entry', {
set_account_currency_and_balance: function(frm, account, currency_field, set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) { balance_field, callback_function) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) { if (frm.doc.posting_date && account) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@ -427,6 +424,14 @@ frappe.ui.form.on('Payment Entry', {
if(!frm.doc.paid_amount && frm.doc.received_amount) if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm); frm.events.received_amount(frm);
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
&& frm.doc.paid_amount != frm.doc.received_amount) {
if (company_currency != frm.doc.paid_from_account_currency &&
frm.doc.payment_type == "Pay") {
frm.doc.paid_amount = frm.doc.received_amount;
}
}
} }
}, },
() => { () => {

View File

@ -8,7 +8,7 @@ from frappe import _
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions, from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions,
get_dimension_filters) get_dimensions)
class PeriodClosingVoucher(AccountsController): class PeriodClosingVoucher(AccountsController):
def validate(self): def validate(self):
@ -58,7 +58,7 @@ class PeriodClosingVoucher(AccountsController):
for dimension in accounting_dimensions: for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension)) dimension_fields.append('t1.{0}'.format(dimension))
dimension_filters, default_dimensions = get_dimension_filters() dimension_filters, default_dimensions = get_dimensions()
pl_accounts = self.get_pl_balances(dimension_fields) pl_accounts = self.get_pl_balances(dimension_fields)

View File

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

View File

@ -57,6 +57,8 @@ frappe.ui.form.on('POS Profile', {
} }
}; };
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
@ -67,6 +69,7 @@ frappe.ui.form.on('POS Profile', {
company: function(frm) { company: function(frm) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
toggle_display_account_head: function(frm) { toggle_display_account_head: function(frm) {

View File

@ -26,6 +26,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}; };
}); });
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
this._super(); this._super();
@ -41,6 +46,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if (this.frm.doc.supplier && this.frm.doc.__islocal) { if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier'); this.frm.trigger('supplier');
} }
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh: function(doc) { refresh: function(doc) {
@ -498,7 +505,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
frappe.ui.form.on("Purchase Invoice", { frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Purchase Invoice': 'Debit Note', 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment'
} }
@ -511,15 +518,6 @@ frappe.ui.form.on("Purchase Invoice", {
} }
} }
} }
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
}, },
onload: function(frm) { onload: function(frm) {

View File

@ -426,32 +426,37 @@ class TestPurchaseInvoice(unittest.TestCase):
) )
def test_total_purchase_cost_for_project(self): def test_total_purchase_cost_for_project(self):
make_project({'project_name':'_Test Project'}) if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}):
project = make_project({'project_name':'_Test Project for Purchase'})
else:
project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"})
existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") from `tabPurchase Invoice Item`
where project = '{0}'
and docstatus=1""".format(project.name))
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project") pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000) existing_purchase_cost + 15000)
pi1 = make_purchase_invoice(qty=10, project="_Test Project") pi1 = make_purchase_invoice(qty=10, project=project.name)
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15500) existing_purchase_cost + 15500)
pi1.cancel() pi1.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000) existing_purchase_cost + 15000)
pi.cancel() pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice_with_perpetual_inventory(self): def test_return_purchase_invoice_with_perpetual_inventory(self):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
@ -860,17 +865,17 @@ class TestPurchaseInvoice(unittest.TestCase):
}) })
pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
pi.items[0].project = item_project.project_name pi.items[0].project = item_project.name
pi.project = project.project_name pi.project = project.name
pi.submit() pi.submit()
expected_values = { expected_values = {
"Creditors - _TC": { "Creditors - _TC": {
"project": project.project_name "project": project.name
}, },
"_Test Account Cost for Goods Sold - _TC": { "_Test Account Cost for Goods Sold - _TC": {
"project": item_project.project_name "project": item_project.name
} }
} }
@ -1026,7 +1031,7 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_return = args.is_return pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC" pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No" pi.is_subcontracted = args.is_subcontracted or "No"
if args.supplier_warehouse: if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", { pi.append("items", {

View File

@ -5,14 +5,17 @@
cur_frm.pformat.print_heading = 'Invoice'; cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({
setup: function(doc) { setup: function(doc) {
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
this._super(doc); this._super(doc);
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
var me = this; var me = this;
this._super(); this._super();
@ -33,6 +36,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
me.frm.refresh_fields(); me.frm.refresh_fields();
} }
erpnext.queries.setup_warehouse_query(this.frm); erpnext.queries.setup_warehouse_query(this.frm);
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh: function(doc, dt, dn) { refresh: function(doc, dt, dn) {
@ -571,15 +575,6 @@ frappe.ui.form.on('Sales Invoice', {
}; };
}); });
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query("unrealized_profit_loss_account", function() { frm.set_query("unrealized_profit_loss_account", function() {
return { return {
filters: { filters: {
@ -592,7 +587,7 @@ frappe.ui.form.on('Sales Invoice', {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery', 'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return', 'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request', 'Payment Request': 'Payment Request',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment'
}, },

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@ -179,7 +179,10 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
@ -261,10 +264,10 @@ class SalesInvoice(SellingController):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@ -549,7 +552,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc) self.against_income_account = ','.join(against_acc)
def add_remarks(self): def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks' if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended
@ -1694,6 +1702,7 @@ def get_mode_of_payment_info(mode_of_payment, company):
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1) (company, mode_of_payment), as_dict=1)
@frappe.whitelist()
def create_dunning(source_name, target_doc=None): def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount

View File

@ -688,7 +688,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gle) self.assertTrue(gle)
def test_pos_gl_entry_with_perpetual_inventory(self): def test_pos_gl_entry_with_perpetual_inventory(self):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
@ -745,7 +745,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos_return.get('payments')[0].amount, -1000) self.assertEqual(pos_return.get('payments')[0].amount, -1000)
def test_pos_change_amount(self): def test_pos_change_amount(self):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
@ -1573,17 +1573,17 @@ class TestSalesInvoice(unittest.TestCase):
}) })
sales_invoice = create_sales_invoice(do_not_save=1) sales_invoice = create_sales_invoice(do_not_save=1)
sales_invoice.items[0].project = item_project.project_name sales_invoice.items[0].project = item_project.name
sales_invoice.project = project.project_name sales_invoice.project = project.name
sales_invoice.submit() sales_invoice.submit()
expected_values = { expected_values = {
"Debtors - _TC": { "Debtors - _TC": {
"project": project.project_name "project": project.name
}, },
"Sales - _TC": { "Sales - _TC": {
"project": item_project.project_name "project": item_project.name
} }
} }
@ -1841,7 +1841,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
def test_einvoice_submission_without_irn(self): def test_einvoice_submission_without_irn(self):
# init # init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1) frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
@ -1857,27 +1857,10 @@ class TestSalesInvoice(unittest.TestCase):
# reset # reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0) frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
frappe.flags.country = country frappe.flags.country = country
def test_einvoice_json(self): def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice from erpnext.regional.india.e_invoice.utils import make_einvoice
customer_gstin = '27AACCM7806M1Z3'
customer_gstin_dtls = {
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
company_gstin = '27AAECE4835E1ZR'
company_gstin_dtls = {
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
# set cache gstin details to avoid fetching details which will require connection to GSP servers
frappe.local.gstin_cache = {}
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'
si.items = [] si.items = []
@ -1930,12 +1913,12 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(value_details['SgstVal'], total_item_sgst_value) self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_value) self.assertEqual(value_details['IgstVal'], total_item_igst_value)
self.assertEqual( calculated_invoice_value = \
value_details['TotInvVal'], value_details['AssVal'] + value_details['CgstVal'] \
value_details['AssVal'] + value_details['CgstVal'] + value_details['SgstVal'] + value_details['IgstVal'] \
+ value_details['SgstVal'] + value_details['IgstVal']
+ value_details['OthChrg'] - value_details['Discount'] + value_details['OthChrg'] - value_details['Discount']
)
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls']) self.assertTrue(einvoice['EwbDtls'])

View File

@ -1,16 +1,18 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on('Shipping Rule', { frappe.provide('erpnext.accounts.dimensions');
refresh: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frappe.ui.form.on('Shipping Rule', {
onload: function(frm) {
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
refresh: function(frm) {
frm.set_query("account", function() { frm.set_query("account", function() {
return { return {
filters: { filters: {

View File

@ -446,7 +446,7 @@ class Subscription(Document):
if not self.generate_invoice_at_period_start: if not self.generate_invoice_at_period_start:
return False return False
if self.is_new_subscription(): if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True return True
# Check invoice dates and make sure it doesn't have outstanding invoices # Check invoice dates and make sure it doesn't have outstanding invoices

View File

@ -47,21 +47,22 @@ def get_data(filters):
for d in gl_entries: for d in gl_entries:
asset_data = assets_details.get(d.against_voucher) asset_data = assets_details.get(d.against_voucher)
if not asset_data.get("accumulated_depreciation_amount"): if asset_data:
asset_data.accumulated_depreciation_amount = d.debit if not asset_data.get("accumulated_depreciation_amount"):
else: asset_data.accumulated_depreciation_amount = d.debit
asset_data.accumulated_depreciation_amount += d.debit else:
asset_data.accumulated_depreciation_amount += d.debit
row = frappe._dict(asset_data) row = frappe._dict(asset_data)
row.update({ row.update({
"depreciation_amount": d.debit, "depreciation_amount": d.debit,
"depreciation_date": d.posting_date, "depreciation_date": d.posting_date,
"amount_after_depreciation": (flt(row.gross_purchase_amount) - "amount_after_depreciation": (flt(row.gross_purchase_amount) -
flt(row.accumulated_depreciation_amount)), flt(row.accumulated_depreciation_amount)),
"depreciation_entry": d.voucher_no "depreciation_entry": d.voucher_no
}) })
data.append(row) data.append(row)
return data return data

View File

@ -53,8 +53,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row = { row = {
'item_code': d.item_code, 'item_code': d.item_code,
'item_name': item_record.item_name, 'item_name': item_record.item_name if item_record else d.item_name,
'item_group': item_record.item_group, 'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description, 'description': d.description,
'invoice': d.parent, 'invoice': d.parent,
'posting_date': d.posting_date, 'posting_date': d.posting_date,
@ -316,6 +316,7 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,

View File

@ -59,23 +59,111 @@ def validate_filters(filters):
def get_columns(filters): def get_columns(filters):
return [ return [
_("Payment Document") + ":: 100", {
_("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", "fieldname": "payment_document",
_("Party Type") + "::100", "label": _("Payment Document Type"),
_("Party") + ":Dynamic Link/Party Type:140", "fieldtype": "Data",
_("Posting Date") + ":Date:100", "width": 100
_("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"), },
_("Invoice Posting Date") + ":Date:130", {
_("Payment Due Date") + ":Date:130", "fieldname": "payment_entry",
_("Debit") + ":Currency:120", "label": _("Payment Document"),
_("Credit") + ":Currency:120", "fieldtype": "Dynamic Link",
_("Remarks") + "::150", "options": "payment_document",
_("Age") +":Int:40", "width": 160
"0-30:Currency:100", },
"30-60:Currency:100", {
"60-90:Currency:100", "fieldname": "party_type",
_("90-Above") + ":Currency:100", "label": _("Party Type"),
_("Delay in payment (Days)") + "::150" "fieldtype": "Data",
"width": 100
},
{
"fieldname": "party",
"label": _("Party"),
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 160
},
{
"fieldname": "posting_date",
"label": _("Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "invoice",
"label": _("Invoice"),
"fieldtype": "Link",
"options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice",
"width": 160
},
{
"fieldname": "invoice_posting_date",
"label": _("Invoice Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "due_date",
"label": _("Payment Due Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "debit",
"label": _("Debit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "credit",
"label": _("Credit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "remarks",
"label": _("Remarks"),
"fieldtype": "Data",
"width": 200
},
{
"fieldname": "age",
"label": _("Age"),
"fieldtype": "Int",
"width": 50
},
{
"fieldname": "range1",
"label": "0-30",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range2",
"label": "30-60",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range3",
"label": "60-90",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range4",
"label": _("90 Above"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "delay_in_payment",
"label": _("Delay in payment (Days)"),
"fieldtype": "Int",
"width": 100
}
] ]
def get_conditions(filters): def get_conditions(filters):

View File

@ -48,7 +48,7 @@ class CropCycle(Document):
def import_disease_tasks(self, disease, start_date): def import_disease_tasks(self, disease, start_date):
disease_doc = frappe.get_doc('Disease', disease) disease_doc = frappe.get_doc('Disease', disease)
self.create_task(disease_doc.treatment_task, self.name, start_date) self.create_task(disease_doc.treatment_task, self.project, start_date)
def create_project(self, period, crop_tasks): def create_project(self, period, crop_tasks):
project = frappe.get_doc({ project = frappe.get_doc({

View File

@ -71,4 +71,4 @@ def check_task_creation():
def check_project_creation(): def check_project_creation():
return True if frappe.db.exists('Project', 'Basil from seed 2017') else False return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False

View File

@ -2,6 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.asset"); frappe.provide("erpnext.asset");
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset', { frappe.ui.form.on('Asset', {
onload: function(frm) { onload: function(frm) {
@ -32,13 +33,11 @@ frappe.ui.form.on('Asset', {
}; };
}); });
frm.set_query("cost_center", function() { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
return { },
"filters": {
"company": frm.doc.company, company: function(frm) {
} erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
};
});
}, },
setup: function(frm) { setup: function(frm) {

View File

@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset Value Adjustment', { frappe.ui.form.on('Asset Value Adjustment', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('company', 'cost_center', 'cost_center'); frm.add_fetch('company', 'cost_center', 'cost_center');
@ -13,11 +15,19 @@ frappe.ui.form.on('Asset Value Adjustment', {
} }
}); });
}, },
onload: function(frm) { onload: function(frm) {
if(frm.is_new() && frm.doc.asset) { if(frm.is_new() && frm.doc.asset) {
frm.trigger("set_current_asset_value"); frm.trigger("set_current_asset_value");
} }
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
asset: function(frm) { asset: function(frm) {
frm.trigger("set_current_asset_value"); frm.trigger("set_current_asset_value");
}, },

View File

@ -13,17 +13,14 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
class AssetValueAdjustment(Document): class AssetValueAdjustment(Document):
def validate(self): def validate(self):
self.validate_date() self.validate_date()
self.set_difference_amount()
self.set_current_asset_value() self.set_current_asset_value()
self.set_difference_amount()
def on_submit(self): def on_submit(self):
self.make_depreciation_entry() self.make_depreciation_entry()
self.reschedule_depreciations(self.new_asset_value) self.reschedule_depreciations(self.new_asset_value)
def on_cancel(self): def on_cancel(self):
if self.journal_entry:
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
self.reschedule_depreciations(self.current_asset_value) self.reschedule_depreciations(self.current_asset_value)
def validate_date(self): def validate_date(self):

View File

@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying"); frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %}; {% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on("Purchase Order", { frappe.ui.form.on("Purchase Order", {
@ -30,6 +30,10 @@ frappe.ui.form.on("Purchase Order", {
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
set_schedule_date(frm); set_schedule_date(frm);
if (!frm.doc.transaction_date){ if (!frm.doc.transaction_date){
@ -39,6 +43,8 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() { erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc); return erpnext.queries.warehouse(frm.doc);
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
} }
}); });
@ -58,8 +64,8 @@ frappe.ui.form.on("Purchase Order Item", {
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() { setup: function() {
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {
'Purchase Receipt': 'Receipt', 'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Invoice', 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier', 'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment', 'Payment Entry': 'Payment',
} }

View File

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

View File

@ -35,9 +35,10 @@ def update_last_purchase_rate(doc, is_submit):
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate # update last purchsae rate
if last_purchase_rate: frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
(flt(last_purchase_rate), d.item_code))
def validate_for_items(doc): def validate_for_items(doc):
items = [] items = []

View File

@ -336,7 +336,7 @@ class BuyingController(StockController):
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0) consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_nos', '') consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '') consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty transferred_qty = raw_material.qty

View File

@ -493,6 +493,41 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
'company': filters.get("company", "") 'company': filters.get("company", "")
}) })
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
meta = frappe.get_meta(doctype)
if meta.is_tree:
query_filters.append(['is_group', '=', 0])
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
if txt:
query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
query_selector = 'in'
else:
query_selector = 'not in'
if len(dimension_filters['allowed_dimensions']) == 1:
dimensions = tuple(dimension_filters['allowed_dimensions'] * 2)
else:
dimensions = tuple(dimension_filters['allowed_dimensions'])
query_filters.append(['name', query_selector, dimensions])
output = frappe.get_all(doctype, filters=query_filters)
result = [d.name for d in output]
return [(d,) for d in set(result)]
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
@ -620,6 +655,34 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters) return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, filters):
query = """
select name
from `tabHealthcare Service Unit`
where
is_group = 0
and company = {company}
and name like {txt}""".format(
company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt)))
if filters and filters.get('inpatient_record'):
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record'))
# if the patient is admitted, then appointments should be allowed against the admission service unit,
# inspite of it being an Inpatient Occupancy service unit
if service_unit:
query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit))
else:
query += " and allow_appointments = 1"
else:
query += " and allow_appointments = 1"
return frappe.db.sql(query, filters)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters): def get_tax_template(doctype, txt, searchfield, start, page_len, filters):

View File

@ -191,7 +191,7 @@ class SellingController(StockController):
for it in self.get("items"): for it in self.get("items"):
if not it.item_code: if not it.item_code:
continue continue
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) 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) 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): if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
@ -233,7 +233,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate 'incoming_rate': p.get("incoming_rate")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@ -252,7 +252,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate 'incoming_rate': d.get("incoming_rate")
})) }))
return il return il
@ -391,7 +391,7 @@ class SellingController(StockController):
}) })
if item_row.warehouse: if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name sle.dependant_sle_voucher_detail_no = item_row.name
return sle return sle
def set_po_nos(self, for_validate=False): def set_po_nos(self, for_validate=False):

View File

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

View File

@ -126,7 +126,7 @@ class Appointment(Document):
add_assignemnt({ add_assignemnt({
'doctype': self.doctype, 'doctype': self.doctype,
'name': self.name, 'name': self.name,
'assign_to': existing_assignee 'assign_to': [existing_assignee]
}) })
return return
if self._assign: if self._assign:
@ -139,7 +139,7 @@ class Appointment(Document):
add_assignemnt({ add_assignemnt({
'doctype': self.doctype, 'doctype': self.doctype,
'name': self.name, 'name': self.name,
'assign_to': agent 'assign_to': [agent]
}) })
break break

View File

@ -8,12 +8,12 @@
"is_mandatory": 0, "is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-14 17:38:27.496696", "modified": "2021-01-21 15:28:52.483839",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create Opportunity", "name": "Create Opportunity",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Opportunity", "reference_document": "Opportunity",
"show_full_form": 0, "show_full_form": 1,
"title": "Create Opportunity", "title": "Create Opportunity",
"validate_action": 1 "validate_action": 1
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Fee Schedule', { frappe.ui.form.on('Fee Schedule', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account'); frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account');
@ -8,6 +9,10 @@ frappe.ui.form.on('Fee Schedule', {
frm.add_fetch('fee_structure', 'cost_center', 'cost_center'); frm.add_fetch('fee_structure', 'cost_center', 'cost_center');
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
frm.set_query('receivable_account', function(doc) { frm.set_query('receivable_account', function(doc) {
return { return {
@ -50,6 +55,8 @@ frappe.ui.form.on('Fee Schedule', {
} }
} }
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -1,6 +1,8 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Fee Structure', { frappe.ui.form.on('Fee Structure', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('company', 'default_receivable_account', 'receivable_account'); frm.add_fetch('company', 'default_receivable_account', 'receivable_account');
@ -8,6 +10,10 @@ frappe.ui.form.on('Fee Structure', {
frm.add_fetch('company', 'cost_center', 'cost_center'); frm.add_fetch('company', 'cost_center', 'cost_center');
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
frm.set_query('academic_term', function() { frm.set_query('academic_term', function() {
return { return {
@ -35,6 +41,8 @@ frappe.ui.form.on('Fee Structure', {
} }
}; };
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Fees", { frappe.ui.form.on("Fees", {
setup: function(frm) { setup: function(frm) {
@ -9,15 +10,19 @@ frappe.ui.form.on("Fees", {
frm.add_fetch("fee_structure", "cost_center", "cost_center"); frm.add_fetch("fee_structure", "cost_center", "cost_center");
}, },
onload: function(frm){ company: function(frm) {
frm.set_query("academic_term",function(){ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) {
frm.set_query("academic_term", function() {
return{ return{
"filters":{ "filters": {
"academic_year": (frm.doc.academic_year) "academic_year": (frm.doc.academic_year)
} }
}; };
}); });
frm.set_query("fee_structure",function(){ frm.set_query("fee_structure", function() {
return{ return{
"filters":{ "filters":{
"academic_year": (frm.doc.academic_year) "academic_year": (frm.doc.academic_year)
@ -45,6 +50,8 @@ frappe.ui.form.on("Fees", {
if (!frm.doc.posting_date) { if (!frm.doc.posting_date) {
frm.doc.posting_date = frappe.datetime.get_today(); frm.doc.posting_date = frappe.datetime.get_today();
} }
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -124,21 +124,24 @@ class ProgramEnrollment(Document):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_program_courses(doctype, txt, searchfield, start, page_len, filters): def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
if filters.get('program'): if not filters.get('program'):
return frappe.db.sql("""select course, course_name from `tabProgram Course` frappe.msgprint(_("Please select a Program first."))
where parent = %(program)s and course like %(txt)s {match_cond} return []
order by
if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), return frappe.db.sql("""select course, course_name from `tabProgram Course`
idx desc, where parent = %(program)s and course like %(txt)s {match_cond}
`tabProgram Course`.course asc order by
limit {start}, {page_len}""".format( if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
match_cond=get_match_cond(doctype), idx desc,
start=start, `tabProgram Course`.course asc
page_len=page_len), { limit {start}, {page_len}""".format(
"txt": "%{0}%".format(txt), match_cond=get_match_cond(doctype),
"_txt": txt.replace('%', ''), start=start,
"program": filters['program'] page_len=page_len), {
}) "txt": "%{0}%".format(txt),
"_txt": txt.replace('%', ''),
"program": filters['program']
})
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -29,14 +29,11 @@ class PlaidConnector():
response = self.client.Item.public_token.exchange(public_token) response = self.client.Item.public_token.exchange(public_token)
access_token = response["access_token"] access_token = response["access_token"]
return access_token return access_token
def get_link_token(self): def get_token_request(self, update_mode=False):
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
token_request = { args = {
"client_name": self.client_name, "client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020) # only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": country_codes, "country_codes": country_codes,
@ -45,6 +42,20 @@ class PlaidConnector():
} }
} }
if update_mode:
args["access_token"] = self.access_token
else:
args.update({
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
})
return args
def get_link_token(self, update_mode=False):
token_request = self.get_token_request(update_mode)
try: try:
response = self.client.LinkToken.create(token_request) response = self.client.LinkToken.create(token_request)
except InvalidRequestError: except InvalidRequestError:

View File

@ -12,9 +12,25 @@ frappe.ui.form.on('Plaid Settings', {
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.enabled) { if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => { frm.add_custom_button(__('Link a new bank account'), () => {
new erpnext.integrations.plaidLink(frm); new erpnext.integrations.plaidLink(frm);
}); });
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
freeze: true,
callback: () => {
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
frappe.msgprint({
title: __("Sync Started"),
message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]),
alert: 1
});
}
});
}).addClass("btn-primary");
} }
} }
}); });
@ -30,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink {
this.product = ["auth", "transactions"]; this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env; this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename; this.client_name = frappe.boot.sitename;
this.token = await this.frm.call("get_link_token").then(resp => resp.message); this.token = await this.get_link_token();
this.init_plaid(); this.init_plaid();
} }
async get_link_token() {
const token = await this.frm.call("get_link_token").then(resp => resp.message);
if (!token) {
frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
}
return token;
}
init_plaid() { init_plaid() {
const me = this; const me = this;
me.loadScript(me.plaidUrl) me.loadScript(me.plaidUrl)
@ -78,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
onScriptError(error) { onScriptError(error) {
frappe.msgprint("There was an issue connecting to Plaid's authentication server"); frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
frappe.msgprint(error); console.log(error);
} }
plaid_success(token, response) { plaid_success(token, response) {
@ -107,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink {
}); });
}, __("Select a company"), __("Continue")); }, __("Select a company"), __("Continue"));
} }
}; };

View File

@ -166,7 +166,6 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True) related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token") access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id account_id = related_bank[0].integration_id
else: else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token") access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None account_id = None
@ -228,13 +227,23 @@ def new_bank_transaction(transaction):
def automatic_synchronization(): def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings") settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1: if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) enqueue_synchronization()
for plaid_account in plaid_accounts: @frappe.whitelist()
frappe.enqueue( def enqueue_synchronization():
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", plaid_accounts = frappe.get_all("Bank Account",
bank=plaid_account.bank, filters={"integration_id": ["!=", ""]},
bank_account=plaid_account.name fields=["name", "bank"])
)
for plaid_account in plaid_accounts:
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)
@frappe.whitelist()
def get_link_token_for_update(access_token):
plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True)

View File

@ -6,3 +6,5 @@ class PartyFrozen(frappe.ValidationError): pass
class InvalidAccountCurrency(frappe.ValidationError): pass class InvalidAccountCurrency(frappe.ValidationError): pass
class InvalidCurrency(frappe.ValidationError): pass class InvalidCurrency(frappe.ValidationError): pass
class PartyDisabled(frappe.ValidationError):pass class PartyDisabled(frappe.ValidationError):pass
class InvalidAccountDimensionError(frappe.ValidationError): pass
class MandatoryAccountDimensionError(frappe.ValidationError): pass

View File

@ -100,7 +100,6 @@ class ClinicalProcedure(Document):
allow_start = self.set_actual_qty() allow_start = self.set_actual_qty()
if allow_start: if allow_start:
self.db_set('status', 'In Progress') self.db_set('status', 'In Progress')
insert_clinical_procedure_to_medical_record(self)
return 'success' return 'success'
return 'insufficient stock' return 'insufficient stock'
@ -247,21 +246,3 @@ def make_procedure(source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doc return doc
def insert_clinical_procedure_to_medical_record(doc):
subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "<br>"
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
if subject and doc.notes:
subject += '<br/>' + doc.notes
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Clinical Procedure'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@ -17,6 +17,9 @@
"enable_free_follow_ups", "enable_free_follow_ups",
"max_visits", "max_visits",
"valid_days", "valid_days",
"inpatient_settings_section",
"allow_discharge_despite_unbilled_services",
"do_not_bill_inpatient_encounters",
"healthcare_service_items", "healthcare_service_items",
"inpatient_visit_charge_item", "inpatient_visit_charge_item",
"op_consulting_charge_item", "op_consulting_charge_item",
@ -302,11 +305,28 @@
"fieldname": "enable_free_follow_ups", "fieldname": "enable_free_follow_ups",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Free Follow-ups" "label": "Enable Free Follow-ups"
},
{
"fieldname": "inpatient_settings_section",
"fieldtype": "Section Break",
"label": "Inpatient Settings"
},
{
"default": "0",
"fieldname": "allow_discharge_despite_unbilled_services",
"fieldtype": "Check",
"label": "Allow Discharge Despite Unbilled Healthcare Services"
},
{
"default": "0",
"fieldname": "do_not_bill_inpatient_encounters",
"fieldtype": "Check",
"label": "Do Not Bill Patient Encounters for Inpatients"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-07-08 15:17:21.543218", "modified": "2021-01-13 09:04:35.877700",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Settings", "name": "Healthcare Settings",

View File

@ -5,6 +5,7 @@ frappe.ui.form.on('Inpatient Medication Entry', {
refresh: function(frm) { refresh: function(frm) {
// Ignore cancellation of doctype on cancel all // Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry']; frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.set_query('item_code', () => { frm.set_query('item_code', () => {
return { return {

View File

@ -139,7 +139,6 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Inpatient Medication Orders", "label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail", "options": "Inpatient Medication Entry Detail",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@ -180,7 +179,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-03 13:22:37.820707", "modified": "2021-01-11 12:37:46.749659",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Inpatient Medication Entry", "name": "Inpatient Medication Entry",

View File

@ -15,8 +15,6 @@ class InpatientMedicationEntry(Document):
self.validate_medication_orders() self.validate_medication_orders()
def get_medication_orders(self): def get_medication_orders(self):
self.validate_datetime_filters()
# pull inpatient medication orders based on selected filters # pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self) orders = get_pending_medication_orders(self)
@ -27,22 +25,6 @@ class InpatientMedicationEntry(Document):
self.set('medication_orders', []) self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria')) frappe.msgprint(_('No pending medication orders found for selected criteria'))
def validate_datetime_filters(self):
if self.from_date and self.to_date:
self.validate_from_to_dates('from_date', 'to_date')
if self.from_date and getdate(self.from_date) > getdate():
frappe.throw(_('From Date cannot be after the current date.'))
if self.to_date and getdate(self.to_date) > getdate():
frappe.throw(_('To Date cannot be after the current date.'))
if self.from_time and self.from_time > nowtime():
frappe.throw(_('From Time cannot be after the current time.'))
if self.to_time and self.to_time > nowtime():
frappe.throw(_('To Time cannot be after the current time.'))
def add_mo_to_table(self, orders): def add_mo_to_table(self, orders):
# Add medication orders in the child table # Add medication orders in the child table
self.set('medication_orders', []) self.set('medication_orders', [])
@ -282,7 +264,7 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record): def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record) ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
if ip_record.inpatient_occupancies: if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit return ip_record.inpatient_occupancies[-1].service_unit
return return

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _ from frappe import _
from frappe.utils import today, now_datetime, getdate, get_datetime from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
@ -113,6 +113,7 @@ def schedule_inpatient(args):
inpatient_record.status = 'Admission Scheduled' inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
def schedule_discharge(args): def schedule_discharge(args):
discharge_order = json.loads(args) discharge_order = json.loads(args)
@ -126,16 +127,19 @@ def schedule_discharge(args):
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
def set_details_from_ip_order(inpatient_record, ip_order): def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order: for key in ip_order:
inpatient_record.set(key, ip_order[key]) inpatient_record.set(key, ip_order[key])
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child: for item in encounter_child:
table = inpatient_record.append(inpatient_record_child) table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'): for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname)) table.set(df.fieldname, item.get(df.fieldname))
def check_out_inpatient(inpatient_record): def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record):
inpatient_occupancy.check_out = now_datetime() inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
def discharge_patient(inpatient_record): def discharge_patient(inpatient_record):
validate_invoiced_inpatient(inpatient_record) validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today() inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged" inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
def validate_invoiced_inpatient(inpatient_record):
pending_invoices = [] def validate_inpatient_invoicing(inpatient_record):
if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
return
pending_invoices = get_pending_invoices(inpatient_record)
if pending_invoices:
message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
formatted_doc_rows = ''
for doctype, docnames in pending_invoices.items():
formatted_doc_rows += """
<td>{0}</td>
<td>{1}</td>
</tr>""".format(doctype, docnames)
message += """
<table class='table'>
<thead>
<th>{0}</th>
<th>{1}</th>
</thead>
{2}
</table>
""".format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
def get_pending_invoices(inpatient_record):
pending_invoices = {}
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
service_unit_names = False service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
if inpatient_occupancy.invoiced != 1: if not inpatient_occupancy.invoiced:
if service_unit_names: if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit service_unit_names += ", " + inpatient_occupancy.service_unit
else: else:
service_unit_names = inpatient_occupancy.service_unit service_unit_names = inpatient_occupancy.service_unit
if service_unit_names: if service_unit_names:
pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")") pending_invoices["Inpatient Occupancy"] = service_unit_names
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"] docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs: for doc in docs:
doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record) doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
if doc_name_list: if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices) pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
if pending_invoices: return pending_invoices
frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
.join(pending_invoices)), title=_('Unbilled Invoices'))
def get_pending_doc(doc, doc_name_list, pending_invoices): def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list: if doc_name_list:
doc_ids = False doc_ids = False
for doc_name in doc_name_list: for doc_name in doc_name_list:
doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids: if doc_ids:
doc_ids += ", "+doc_name.name doc_ids += ", " + doc_link
else: else:
doc_ids = doc_name.name doc_ids = doc_link
if doc_ids: if doc_ids:
pending_invoices.append(doc + " (" + doc_ids + ")") pending_invoices[doc] = doc_ids
return pending_invoices return pending_invoices
def get_inpatient_docs_not_invoiced(doc, inpatient_record):
def get_unbilled_inpatient_docs(doc, inpatient_record):
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient, return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted' inpatient_record.status = 'Admitted'
@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted') frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
def transfer_patient(inpatient_record, service_unit, check_in): def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {}) item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit item_line.service_unit = service_unit
@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in):
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied") frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
def patient_leave_service_unit(inpatient_record, check_out, leave_from): def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from):
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters): def get_leave_from(doctype, txt, searchfield, start, page_len, filters):

View File

@ -8,6 +8,8 @@ import unittest
from frappe.utils import now_datetime, today from frappe.utils import now_datetime, today
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.lab_test.test_lab_test import create_patient_encounter
from erpnext.healthcare.utils import get_encounters_to_invoice
class TestInpatientRecord(unittest.TestCase): class TestInpatientRecord(unittest.TestCase):
def test_admit_and_discharge(self): def test_admit_and_discharge(self):
@ -40,6 +42,60 @@ class TestInpatientRecord(unittest.TestCase):
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
def test_allow_discharge_despite_unbilled_services(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=1)
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
# Discharge
schedule_discharge(frappe.as_json({"patient": patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
# Should not validate Pending Invoices
ip_record.discharge()
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=0)
def test_do_not_bill_patient_encounters_for_inpatients(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=1)
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
# Patient Encounter
patient_encounter = create_patient_encounter()
encounters = get_encounters_to_invoice(patient, "_Test Company")
encounter_ids = [entry.reference_name for entry in encounters]
self.assertFalse(patient_encounter.name in encounter_ids)
# Discharge
schedule_discharge(frappe.as_json({"patient": patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record)
discharge_patient(ip_record)
setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0)
def test_validate_overlap_admission(self): def test_validate_overlap_admission(self):
frappe.db.sql("""delete from `tabInpatient Record`""") frappe.db.sql("""delete from `tabInpatient Record`""")
patient = create_patient() patient = create_patient()
@ -63,6 +119,13 @@ def mark_invoiced_inpatient_occupancy(ip_record):
inpatient_occupancy.invoiced = 1 inpatient_occupancy.invoiced = 1
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
def setup_inpatient_settings(key, value):
settings = frappe.get_single("Healthcare Settings")
settings.set(key, value)
settings.save()
def create_inpatient(patient): def create_inpatient(patient):
patient_obj = frappe.get_doc('Patient', patient) patient_obj = frappe.get_doc('Patient', patient)
inpatient_record = frappe.new_doc('Inpatient Record') inpatient_record = frappe.new_doc('Inpatient Record')
@ -78,11 +141,16 @@ def create_inpatient(patient):
inpatient_record.scheduled_date = today() inpatient_record.scheduled_date = today()
return inpatient_record return inpatient_record
def get_healthcare_service_unit():
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) def get_healthcare_service_unit(unit_name=None):
if not unit_name:
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
else:
service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name})
if not service_unit: if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit") service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy" service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company" service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type() service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1 service_unit.inpatient_occupancy = 1
@ -105,6 +173,7 @@ def get_healthcare_service_unit():
return service_unit.name return service_unit.name
return service_unit return service_unit
def get_service_unit_type(): def get_service_unit_type():
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1}) service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1})
@ -116,6 +185,7 @@ def get_service_unit_type():
return service_unit_type.name return service_unit_type.name
return service_unit_type return service_unit_type
def create_patient(): def create_patient():
patient = frappe.db.exists('Patient', '_Test IPD Patient') patient = frappe.db.exists('Patient', '_Test IPD Patient')
if not patient: if not patient:

View File

@ -359,6 +359,7 @@
{ {
"fieldname": "normal_test_items", "fieldname": "normal_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Normal Test Result",
"options": "Normal Test Result", "options": "Normal Test Result",
"print_hide": 1 "print_hide": 1
}, },
@ -380,6 +381,7 @@
{ {
"fieldname": "sensitivity_test_items", "fieldname": "sensitivity_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sensitivity Test Result",
"options": "Sensitivity Test Result", "options": "Sensitivity Test Result",
"print_hide": 1, "print_hide": 1,
"report_hide": 1 "report_hide": 1
@ -529,6 +531,7 @@
{ {
"fieldname": "descriptive_test_items", "fieldname": "descriptive_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Descriptive Test Result",
"options": "Descriptive Test Result", "options": "Descriptive Test Result",
"print_hide": 1, "print_hide": 1,
"report_hide": 1 "report_hide": 1
@ -549,13 +552,14 @@
{ {
"fieldname": "organism_test_items", "fieldname": "organism_test_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Organism Test Result",
"options": "Organism Test Result", "options": "Organism Test Result",
"print_hide": 1 "print_hide": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-30 18:18:38.516215", "modified": "2020-11-30 11:04:17.195848",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Lab Test", "name": "Lab Test",

View File

@ -17,11 +17,9 @@ class LabTest(Document):
self.validate_result_values() self.validate_result_values()
self.db_set('submitted_date', getdate()) self.db_set('submitted_date', getdate())
self.db_set('status', 'Completed') self.db_set('status', 'Completed')
insert_lab_test_to_medical_record(self)
def on_cancel(self): def on_cancel(self):
self.db_set('status', 'Cancelled') self.db_set('status', 'Cancelled')
delete_lab_test_from_medical_record(self)
self.reload() self.reload()
def on_update(self): def on_update(self):
@ -330,60 +328,6 @@ def get_employee_by_user_id(user_id):
return frappe.get_doc('Employee', emp_id) return frappe.get_doc('Employee', emp_id)
return None return None
def insert_lab_test_to_medical_record(doc):
table_row = False
subject = cstr(doc.lab_test_name)
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: '))+ doc.practitioner + '<br>'
if doc.normal_test_items:
item = doc.normal_test_items[0]
comment = ''
if item.lab_test_comment:
comment = str(item.lab_test_comment)
table_row = frappe.bold(_('Lab Test Conducted: ')) + item.lab_test_name
if item.lab_test_event:
table_row += frappe.bold(_('Lab Test Event: ')) + item.lab_test_event
if item.result_value:
table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value
if item.normal_range:
table_row += ' ' + _('Normal Range: ') + item.normal_range
table_row += ' ' + comment
elif doc.descriptive_test_items:
item = doc.descriptive_test_items[0]
if item.lab_test_particulars and item.result_value:
table_row = item.lab_test_particulars + ' ' + item.result_value
elif doc.sensitivity_test_items:
item = doc.sensitivity_test_items[0]
if item.antibiotic and item.antibiotic_sensitivity:
table_row = item.antibiotic + ' ' + item.antibiotic_sensitivity
if table_row:
subject += '<br>' + table_row
if doc.lab_test_comment:
subject += '<br>' + cstr(doc.lab_test_comment)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.result_date
medical_record.reference_doctype = 'Lab Test'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions = True)
def delete_lab_test_from_medical_record(self):
medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name))
if medical_record_id and medical_record_id[0][0]:
frappe.delete_doc('Patient Medical Record', medical_record_id[0][0])
@frappe.whitelist() @frappe.whitelist()
def get_lab_test_prescribed(patient): def get_lab_test_prescribed(patient):

View File

@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', {
filters: {'status': 'Active'} filters: {'status': 'Active'}
}; };
}); });
frm.set_query('practitioner', function() { frm.set_query('practitioner', function() {
return { return {
filters: { filters: {
@ -29,16 +30,27 @@ frappe.ui.form.on('Patient Appointment', {
} }
}; };
}); });
frm.set_query('service_unit', function(){
frm.set_query('service_unit', function() {
return { return {
query: 'erpnext.controllers.queries.get_healthcare_service_units',
filters: { filters: {
'is_group': false, company: frm.doc.company,
'allow_appointments': true, inpatient_record: frm.doc.inpatient_record
'company': frm.doc.company
} }
}; };
}); });
frm.set_query('therapy_plan', function() {
return {
filters: {
'patient': frm.doc.patient
}
};
});
frm.trigger('set_therapy_type_filter');
if (frm.is_new()) { if (frm.is_new()) {
frm.page.set_primary_action(__('Check Availability'), function() { frm.page.set_primary_action(__('Check Availability'), function() {
if (!frm.doc.patient) { if (!frm.doc.patient) {
@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', {
} }
}, },
therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter');
},
set_therapy_type_filter: function(frm) {
if (frm.doc.therapy_plan) {
frm.call('get_therapy_types').then(r => {
frm.set_query('therapy_type', function() {
return {
filters: {
'name': ['in', r.message]
}
};
});
});
}
},
therapy_type: function(frm) { therapy_type: function(frm) {
if (frm.doc.therapy_type) { if (frm.doc.therapy_type) {
frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {

View File

@ -23,9 +23,9 @@
"procedure_template", "procedure_template",
"get_procedure_from_encounter", "get_procedure_from_encounter",
"procedure_prescription", "procedure_prescription",
"therapy_plan",
"therapy_type", "therapy_type",
"get_prescribed_therapies", "get_prescribed_therapies",
"therapy_plan",
"practitioner", "practitioner",
"practitioner_name", "practitioner_name",
"department", "department",
@ -284,7 +284,7 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"depends_on": "eval:doc.patient;", "depends_on": "eval:doc.patient && doc.therapy_plan;",
"fieldname": "therapy_type", "fieldname": "therapy_type",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Therapy", "label": "Therapy",
@ -292,17 +292,16 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"depends_on": "eval:doc.patient && doc.__islocal;", "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;",
"fieldname": "get_prescribed_therapies", "fieldname": "get_prescribed_therapies",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Get Prescribed Therapies" "label": "Get Prescribed Therapies"
}, },
{ {
"depends_on": "eval: doc.patient && doc.therapy_type", "depends_on": "eval: doc.patient;",
"fieldname": "therapy_plan", "fieldname": "therapy_plan",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Therapy Plan", "label": "Therapy Plan",
"mandatory_depends_on": "eval: doc.patient && doc.therapy_type",
"options": "Therapy Plan" "options": "Therapy Plan"
}, },
{ {
@ -348,7 +347,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-05-21 03:04:21.400893", "modified": "2020-12-16 13:16:58.578503",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Appointment", "name": "Patient Appointment",

View File

@ -18,6 +18,7 @@ from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_pr
class PatientAppointment(Document): class PatientAppointment(Document):
def validate(self): def validate(self):
self.validate_overlaps() self.validate_overlaps()
self.validate_service_unit()
self.set_appointment_datetime() self.set_appointment_datetime()
self.validate_customer_created() self.validate_customer_created()
self.set_status() self.set_status()
@ -68,6 +69,19 @@ class PatientAppointment(Document):
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4]) overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping')) frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
is_inpatient_occupancy_unit = frappe.db.get_value('Healthcare Service Unit', self.service_unit,
'inpatient_occupancy')
service_unit = get_current_healthcare_service_unit(self.inpatient_record)
if is_inpatient_occupancy_unit and service_unit != self.service_unit:
msg = _('Patient {0} is not admitted in the service unit {1}').format(frappe.bold(self.patient), frappe.bold(self.service_unit)) + '<br>'
msg += _('Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted.')
frappe.throw(msg, title=_('Invalid Healthcare Service Unit'))
def set_appointment_datetime(self): def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
@ -91,6 +105,17 @@ class PatientAppointment(Document):
if fee_validity: if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
def get_therapy_types(self):
if not self.therapy_plan:
return
therapy_types = []
doc = frappe.get_doc('Therapy Plan', self.therapy_plan)
for entry in doc.therapy_plan_details:
therapy_types.append(entry.therapy_type)
return therapy_types
@frappe.whitelist() @frappe.whitelist()
def check_payment_fields_reqd(patient): def check_payment_fields_reqd(patient):
@ -145,7 +170,7 @@ def invoice_appointment(appointment_doc):
sales_invoice.flags.ignore_mandatory = True sales_invoice.flags.ignore_mandatory = True
sales_invoice.save(ignore_permissions=True) sales_invoice.save(ignore_permissions=True)
sales_invoice.submit() sales_invoice.submit()
frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True) frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
from frappe.utils import nowdate, add_days from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEquals(appointment.status, 'Open') self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled') self.assertEquals(appointment.status, 'Scheduled')
create_encounter(appointment) encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
encounter.cancel()
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self): def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
@ -76,6 +78,59 @@ class TestPatientAppointment(unittest.TestCase):
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled') self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled')
def test_appointment_booking_for_admission_service_unit(self):
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
self.assertEqual(appointment.service_unit, service_unit)
# Discharge
schedule_discharge(frappe.as_json({'patient': patient}))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def test_invalid_healthcare_service_unit_validation(self):
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
# Discharge
schedule_discharge(frappe.as_json({'patient': patient}))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def create_healthcare_docs(): def create_healthcare_docs():
patient = create_patient() patient = create_patient()
@ -123,7 +178,7 @@ def create_encounter(appointment):
encounter.submit() encounter.submit()
return encounter return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0): def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
item = create_healthcare_service_items() item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
@ -134,12 +189,15 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.appointment_date = appointment_date appointment.appointment_date = appointment_date
appointment.company = '_Test Company' appointment.company = '_Test Company'
appointment.duration = 15 appointment.duration = 15
if service_unit:
appointment.service_unit = service_unit
if invoice: if invoice:
appointment.mode_of_payment = 'Cash' appointment.mode_of_payment = 'Cash'
appointment.paid_amount = 500 appointment.paid_amount = 500
if procedure_template: if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name') appointment.procedure_template = create_clinical_procedure_template().get('name')
appointment.save(ignore_permissions=True) if save:
appointment.save(ignore_permissions=True)
return appointment return appointment
def create_healthcare_service_items(): def create_healthcare_service_items():
@ -150,6 +208,7 @@ def create_healthcare_service_items():
item.item_name = 'Consulting Charges' item.item_name = 'Consulting Charges'
item.item_group = 'Services' item.item_group = 'Services'
item.is_stock_item = 0 item.is_stock_item = 0
item.stock_uom = 'Nos'
item.save() item.save()
return item.name return item.name

View File

@ -210,7 +210,7 @@
{ {
"fieldname": "drug_prescription", "fieldname": "drug_prescription",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items", "label": "Drug Prescription",
"options": "Drug Prescription" "options": "Drug Prescription"
}, },
{ {
@ -328,7 +328,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-16 21:00:08.644531", "modified": "2020-11-30 10:39:00.783119",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Encounter", "name": "Patient Encounter",

View File

@ -17,10 +17,6 @@ class PatientEncounter(Document):
def on_update(self): def on_update(self):
if self.appointment: if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
update_encounter_medical_record(self)
def after_insert(self):
insert_encounter_to_medical_record(self)
def on_submit(self): def on_submit(self):
if self.therapies: if self.therapies:
@ -33,8 +29,6 @@ class PatientEncounter(Document):
if self.inpatient_record and self.drug_prescription: if self.inpatient_record and self.drug_prescription:
delete_ip_medication_order(self) delete_ip_medication_order(self)
delete_medical_record(self)
def set_title(self): def set_title(self):
self.title = _('{0} with {1}').format(self.patient_name or self.patient, self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100] self.practitioner_name or self.practitioner)[:100]
@ -102,61 +96,7 @@ def create_therapy_plan(encounter):
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True) frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
def insert_encounter_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.encounter_date
medical_record.reference_doctype = 'Patient Encounter'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_encounter_medical_record(encounter):
medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
if medical_record_id and medical_record_id[0][0]:
subject = set_subject_field(encounter)
frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
else:
insert_encounter_to_medical_record(encounter)
def delete_medical_record(encounter):
record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name})
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def delete_ip_medication_order(encounter): def delete_ip_medication_order(encounter):
record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name}) record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name})
if record: if record:
frappe.delete_doc('Inpatient Medication Order', record, force=1) frappe.delete_doc('Inpatient Medication Order', record, force=1)
def set_subject_field(encounter):
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'
if encounter.symptoms:
subject += frappe.bold(_('Symptoms: ')) + '<br>'
for entry in encounter.symptoms:
subject += cstr(entry.complaint) + '<br>'
else:
subject += frappe.bold(_('No Symptoms')) + '<br>'
if encounter.diagnosis:
subject += frappe.bold(_('Diagnosis: ')) + '<br>'
for entry in encounter.diagnosis:
subject += cstr(entry.diagnosis) + '<br>'
else:
subject += frappe.bold(_('No Diagnosis')) + '<br>'
if encounter.drug_prescription:
subject += '<br>' + _('Drug(s) Prescribed.')
if encounter.lab_test_prescription:
subject += '<br>' + _('Test(s) Prescribed.')
if encounter.procedure_prescription:
subject += '<br>' + _('Procedure(s) Prescribed.')
return subject

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2020-11-25 13:40:23.054469",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"date_fieldname",
"add_edit_fields",
"selected_fields"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "selected_fields",
"fieldtype": "Code",
"label": "Selected Fields",
"read_only": 1
},
{
"fieldname": "add_edit_fields",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Add / Edit Fields"
},
{
"fieldname": "date_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Date Fieldname",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-30 13:54:37.474671",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Custom Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PatientHistoryCustomDocumentType(Document):
pass

View File

@ -0,0 +1,133 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Patient History Settings', {
refresh: function(frm) {
frm.set_query('document_type', 'custom_doctypes', () => {
return {
filters: {
custom: 1,
is_submittable: 1,
module: 'Healthcare',
}
};
});
},
field_selector: function(frm, doc, standard=1) {
let document_fields = [];
if (doc.selected_fields)
document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname);
frm.call({
method: 'get_doctype_fields',
doc: frm.doc,
args: {
document_type: doc.document_type,
fields: document_fields
},
freeze: true,
callback: function(r) {
if (r.message) {
let doctype = 'Patient History Custom Document Type';
if (standard)
doctype = 'Patient History Standard Document Type';
frm.events.show_field_selector_dialog(frm, doc, doctype, r.message);
}
}
});
},
show_field_selector_dialog: function(frm, doc, doctype, doc_fields) {
let d = new frappe.ui.Dialog({
title: __('{0} Fields', [__(doc.document_type)]),
fields: [
{
label: __('Select Fields'),
fieldtype: 'MultiCheck',
fieldname: 'fields',
options: doc_fields,
columns: 2
}
]
});
d.$body.prepend(`
<div class="columns-search">
<input type="text" placeholder="${__('Search')}" data-element="search" class="form-control input-xs">
</div>`
);
frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area');
d.set_primary_action(__('Save'), () => {
let values = d.get_values().fields;
let selected_fields = [];
frappe.model.with_doctype(doc.document_type, function() {
for (let idx in values) {
let value = values[idx];
let field = frappe.get_meta(doc.document_type).fields.filter((df) => df.fieldname == value)[0];
if (field) {
selected_fields.push({
label: field.label,
fieldname: field.fieldname,
fieldtype: field.fieldtype
});
}
}
d.refresh();
frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields));
});
d.hide();
});
d.show();
},
get_date_field_for_dt: function(frm, row) {
frm.call({
method: 'get_date_field_for_dt',
doc: frm.doc,
args: {
document_type: row.document_type
},
callback: function(data) {
if (data.message) {
frappe.model.set_value('Patient History Custom Document Type',
row.name, 'date_fieldname', data.message);
}
}
});
}
});
frappe.ui.form.on('Patient History Custom Document Type', {
document_type: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.get_date_field_for_dt(frm, row);
}
},
add_edit_fields: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.field_selector(frm, row, 0);
}
}
});
frappe.ui.form.on('Patient History Standard Document Type', {
add_edit_fields: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.document_type) {
frm.events.field_selector(frm, row);
}
}
});

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2020-11-25 13:41:37.675518",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"standard_doctypes",
"section_break_2",
"custom_doctypes"
],
"fields": [
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "custom_doctypes",
"fieldtype": "Table",
"label": "Custom Document Types",
"options": "Patient History Custom Document Type"
},
{
"fieldname": "standard_doctypes",
"fieldtype": "Table",
"label": "Standard Document Types",
"options": "Patient History Standard Document Type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-11-25 13:43:38.511771",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.utils import cstr, cint
from frappe.model.document import Document
from erpnext.healthcare.page.patient_history.patient_history import get_patient_history_doctypes
class PatientHistorySettings(Document):
def validate(self):
self.validate_submittable_doctypes()
self.validate_date_fieldnames()
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(
entry.idx, frappe.bold(entry.document_type))
msg += _('Patient Medical Record can only be created for submittable document types.')
frappe.throw(msg)
def validate_date_fieldnames(self):
for entry in self.custom_doctypes:
field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname)
if not field:
frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
if field.fieldtype not in ['Date', 'Datetime']:
frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
def get_doctype_fields(self, document_type, fields):
multicheck_fields = []
doc_fields = frappe.get_meta(document_type).fields
for field in doc_fields:
if field.fieldtype not in frappe.model.no_value_fields or \
field.fieldtype in frappe.model.table_fields and not field.hidden:
multicheck_fields.append({
'label': field.label,
'value': field.fieldname,
'checked': 1 if field.fieldname in fields else 0
})
return multicheck_fields
def get_date_field_for_dt(self, document_type):
meta = frappe.get_meta(document_type)
date_fields = meta.get('fields', {
'fieldtype': ['in', ['Date', 'Datetime']]
})
if date_fields:
return date_fields[0].get('fieldname')
def create_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }):
return
subject = set_subject_field(doc)
date_field = get_date_field(doc.doctype)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.get(date_field)
medical_record.reference_doctype = doc.doctype
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
if medical_record_id:
subject = set_subject_field(doc)
frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
else:
create_medical_record(doc)
def delete_medical_record(doc, method=None):
medical_record_required = validate_medical_record_required(doc)
if not medical_record_required:
return
record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def set_subject_field(doc):
from frappe.utils.formatters import format_value
meta = frappe.get_meta(doc.doctype)
subject = ''
patient_history_fields = get_patient_history_fields(doc)
for entry in patient_history_fields:
fieldname = entry.get('fieldname')
if 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')) + ': ') + '<br>' + cstr(formatted_value) + '<br>'
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) + '<br>'
return subject
def get_date_field(doctype):
dt = get_patient_history_config_dt(doctype)
return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname')
def get_patient_history_fields(doc):
dt = get_patient_history_config_dt(doc.doctype)
patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields')
if patient_history_fields:
return json.loads(patient_history_fields)
def get_formatted_value_for_table_field(items, df):
child_meta = frappe.get_meta(df.options)
table_head = ''
table_row = ''
html = ''
create_head = True
for item in items:
table_row += '<tr>'
for cdf in child_meta.fields:
if cdf.in_list_view:
if create_head:
table_head += '<td>' + cdf.label + '</td>'
if item.get(cdf.fieldname):
table_row += '<td>' + str(item.get(cdf.fieldname)) + '</td>'
else:
table_row += '<td></td>'
create_head = False
table_row += '</tr>'
html += "<table class='table table-condensed table-bordered'>" + table_head + table_row + "</table>"
return html
def get_patient_history_config_dt(doctype):
if frappe.db.get_value('DocType', doctype, 'custom'):
return 'Patient History Custom Document Type'
else:
return 'Patient History Standard Document Type'
def validate_medical_record_required(doc):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \
or get_module(doc) != 'Healthcare':
return False
if doc.doctype not in get_patient_history_doctypes():
return False
return True
def get_module(doc):
module = doc.meta.module
if not module:
module = frappe.db.get_value('DocType', doc.doctype, 'module')
return module

View File

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import json
from frappe.utils import getdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase):
def setUp(self):
dt = create_custom_doctype()
settings = frappe.get_single("Patient History Settings")
settings.append("custom_doctypes", {
"document_type": dt.name,
"date_fieldname": "date",
"selected_fields": json.dumps([{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date"
},
{
"label": "Rating",
"fieldname": "rating",
"fieldtype": "Rating"
},
{
"label": "Feedback",
"fieldname": "feedback",
"fieldtype": "Small Text"
}])
})
settings.save()
def test_custom_doctype_medical_record(self):
# 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 = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format(
frappe.utils.format_date(getdate()))
self.assertEqual(medical_rec.subject, expected_subject)
self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate())
def create_custom_doctype():
if not frappe.db.exists("DocType", "Test Patient Feedback"):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Healthcare",
"custom": 1,
"is_submittable": 1,
"fields": [{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date"
},
{
"label": "Patient",
"fieldname": "patient",
"fieldtype": "Link",
"options": "Patient"
},
{
"label": "Rating",
"fieldname": "rating",
"fieldtype": "Rating"
},
{
"label": "Feedback",
"fieldname": "feedback",
"fieldtype": "Small Text"
}],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Patient Feedback",
})
doc.insert()
return doc
else:
return frappe.get_doc("DocType", "Test Patient Feedback")
def create_doc(patient):
doc = frappe.get_doc({
"doctype": "Test Patient Feedback",
"patient": patient,
"date": getdate(),
"rating": 3,
"feedback": "Test Patient History Settings"
}).insert()
doc.submit()
return doc

View File

@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2020-11-25 13:39:36.014814",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"date_fieldname",
"add_edit_fields",
"selected_fields"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "selected_fields",
"fieldtype": "Code",
"label": "Selected Fields",
"read_only": 1
},
{
"fieldname": "add_edit_fields",
"fieldtype": "Button",
"in_list_view": 1,
"label": "Add / Edit Fields"
},
{
"fieldname": "date_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Date Fieldname",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-30 13:54:56.773325",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient History Standard Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PatientHistoryStandardDocumentType(Document):
pass

View File

@ -18,6 +18,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment) encounter = create_encounter(appointment)
# check for encounter # check for encounter
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name}) medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name})
self.assertTrue(medical_rec) self.assertTrue(medical_rec)

View File

@ -5,10 +5,10 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import getdate, flt from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
class TestTherapyPlan(unittest.TestCase): class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self): def test_creation_on_encounter_submission(self):
@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit() frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session = frappe.get_doc(session)
session.submit()
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
session.cancel()
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_therapy_plan_from_template(self): def test_therapy_plan_from_template(self):
patient = create_patient() patient = create_patient()
template = create_therapy_plan_template() template = create_therapy_plan_template()

View File

@ -47,7 +47,7 @@ class TherapyPlan(Document):
@frappe.whitelist() @frappe.whitelist()
def make_therapy_session(therapy_plan, patient, therapy_type, company): def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None):
therapy_type = frappe.get_doc('Therapy Type', therapy_type) therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session') therapy_session = frappe.new_doc('Therapy Session')
@ -58,6 +58,7 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company):
therapy_session.duration = therapy_type.default_duration therapy_session.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
if frappe.flags.in_test: if frappe.flags.in_test:
therapy_session.start_date = today() therapy_session.start_date = today()

View File

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

View File

@ -41,9 +41,15 @@ class TherapySession(Document):
def on_submit(self): def on_submit(self):
self.update_sessions_count_in_therapy_plan() self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)
def on_update(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
def on_cancel(self): def on_cancel(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
self.update_sessions_count_in_therapy_plan(on_cancel=True) self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False): def update_sessions_count_in_therapy_plan(self, on_cancel=False):
@ -135,23 +141,3 @@ def get_therapy_item(therapy, item):
item.reference_dt = 'Therapy Session' item.reference_dt = 'Therapy Session'
item.reference_dn = therapy.name item.reference_dn = therapy.name
return item return item
def insert_session_medical_record(doc):
subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '<br>'
if doc.therapy_plan:
subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '<br>'
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '<br>'
subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '<br>'
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Therapy Session'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@ -12,47 +12,7 @@ class VitalSigns(Document):
def validate(self): def validate(self):
self.set_title() self.set_title()
def on_submit(self):
insert_vital_signs_to_medical_record(self)
def on_cancel(self):
delete_vital_signs_from_medical_record(self)
def set_title(self): def set_title(self):
self.title = _('{0} on {1}').format(self.patient_name or self.patient, self.title = _('{0} on {1}').format(self.patient_name or self.patient,
frappe.utils.format_date(self.signs_date))[:100] frappe.utils.format_date(self.signs_date))[:100]
def insert_vital_signs_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.signs_date
medical_record.reference_doctype = 'Vital Signs'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.flags.ignore_mandatory = True
medical_record.save(ignore_permissions=True)
def delete_vital_signs_from_medical_record(doc):
medical_record = frappe.db.get_value('Patient Medical Record', {'reference_name': doc.name})
if medical_record:
frappe.delete_doc('Patient Medical Record', medical_record)
def set_subject_field(doc):
subject = ''
if doc.temperature:
subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '<br>'
if doc.pulse:
subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '<br>'
if doc.respiratory_rate:
subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '<br>'
if doc.bp:
subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '<br>'
if doc.bmi:
subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '<br>'
if doc.nutrition_note:
subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '<br>'
return subject

View File

@ -109,6 +109,11 @@
padding-right: 0px; padding-right: 0px;
} }
.patient-history-filter {
margin-left: 35px;
width: 25%;
}
#page-medical_record .plot-wrapper { #page-medical_record .plot-wrapper {
padding: 20px 15px; padding: 20px 15px;
border-bottom: 1px solid #d1d8dd; border-bottom: 1px solid #d1d8dd;

View File

@ -1,6 +1,5 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="col-sm-3"> <div class="col-sm-3">
<p class="text-center">{%= __("Select Patient") %}</p>
<p class="patient" style="margin: auto; max-width: 300px; margin-bottom: 20px;"></p> <p class="patient" style="margin: auto; max-width: 300px; margin-bottom: 20px;"></p>
<div class="patient_details" style="z-index=0"></div> <div class="patient_details" style="z-index=0"></div>
</div> </div>
@ -11,6 +10,13 @@
<div id="chart" class="col-sm-12 patient_vital_charts"> <div id="chart" class="col-sm-12 patient_vital_charts">
</div> </div>
</div> </div>
<div class="header-separator col-sm-12 d-flex border-bottom py-3" style="display:none"></div>
<div class="row">
<div class="col-sm-12 d-flex">
<div class="patient-history-filter doctype-filter"></div>
<div class="patient-history-filter date-filter"></div>
</div>
</div>
<div class="col-sm-12 patient_documents_list"> <div class="col-sm-12 patient_documents_list">
</div> </div>
<div class="col-sm-12 text-center py-3"> <div class="col-sm-12 text-center py-3">

View File

@ -1,141 +1,225 @@
frappe.provide("frappe.patient_history"); frappe.provide('frappe.patient_history');
frappe.pages['patient_history'].on_page_load = function(wrapper) { frappe.pages['patient_history'].on_page_load = function(wrapper) {
var me = this; let me = this;
var page = frappe.ui.make_app_page({ let page = frappe.ui.make_app_page({
parent: wrapper, parent: wrapper,
title: 'Patient History', title: 'Patient History',
single_column: true single_column: true
}); });
frappe.breadcrumbs.add("Healthcare"); frappe.breadcrumbs.add('Healthcare');
let pid = ''; let pid = '';
page.main.html(frappe.render_template("patient_history", {})); page.main.html(frappe.render_template('patient_history', {}));
var patient = frappe.ui.form.make_control({ page.main.find('.header-separator').hide();
parent: page.main.find(".patient"),
let patient = frappe.ui.form.make_control({
parent: page.main.find('.patient'),
df: { df: {
fieldtype: "Link", fieldtype: 'Link',
options: "Patient", options: 'Patient',
fieldname: "patient", fieldname: 'patient',
change: function(){ placeholder: __('Select Patient'),
if(pid != patient.get_value() && patient.get_value()){ only_select: true,
change: function() {
let patient_id = patient.get_value();
if (pid != patient_id && patient_id) {
me.start = 0; me.start = 0;
me.page.main.find(".patient_documents_list").html(""); me.page.main.find('.patient_documents_list').html('');
get_documents(patient.get_value(), me); setup_filters(patient_id, me);
show_patient_info(patient.get_value(), me); get_documents(patient_id, me);
show_patient_vital_charts(patient.get_value(), me, "bp", "mmHg", "Blood Pressure"); show_patient_info(patient_id, me);
show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure');
} }
pid = patient.get_value(); pid = patient_id;
} }
}, },
only_input: true,
}); });
patient.refresh(); patient.refresh();
if (frappe.route_options){ if (frappe.route_options) {
patient.set_value(frappe.route_options.patient); patient.set_value(frappe.route_options.patient);
} }
this.page.main.on("click", ".btn-show-chart", function() { this.page.main.on('click', '.btn-show-chart', function() {
var btn_show_id = $(this).attr("data-show-chart-id"), pts = $(this).attr("data-pts"); let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts');
var title = $(this).attr("data-title"); let title = $(this).attr('data-title');
show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title); show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title);
}); });
this.page.main.on("click", ".btn-more", function() { this.page.main.on('click', '.btn-more', function() {
var doctype = $(this).attr("data-doctype"), docname = $(this).attr("data-docname"); 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"){ 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).hide();
me.page.main.find("."+docname).parent().find('.document-html').show(); me.page.main.find('.'+docname).parent().find('.document-html').show();
}else{ } else {
if(doctype && docname){ if (doctype && docname) {
let exclude = ["patient", "patient_name", 'patient_sex', "encounter_date"]; let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date'];
frappe.call({ frappe.call({
method: "erpnext.healthcare.utils.render_doc_as_html", method: 'erpnext.healthcare.utils.render_doc_as_html',
args:{ args:{
doctype: doctype, doctype: doctype,
docname: docname, docname: docname,
exclude_fields: exclude exclude_fields: exclude
}, },
freeze: true,
callback: function(r) { callback: function(r) {
if (r.message){ if (r.message) {
me.page.main.find("."+docname).hide(); me.page.main.find('.' + docname).hide();
me.page.main.find("."+docname).parent().find('.document-html').html(r.message.html+"\
<div align='center'><a class='btn octicon octicon-chevron-up btn-default btn-xs\ me.page.main.find('.' + docname).parent().find('.document-html').html(
btn-less' data-doctype='"+doctype+"' data-docname='"+docname+"'></a></div>"); `${r.message.html}
me.page.main.find("."+docname).parent().find('.document-html').show(); <div align='center'>
me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched', "1"); <a class='btn octicon octicon-chevron-up btn-default btn-xs btn-less'
data-doctype='${doctype}'
data-docname='${docname}'>
</a>
</div>
`);
me.page.main.find('.' + docname).parent().find('.document-html').show();
me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1');
} }
}, }
freeze: true
}); });
} }
} }
}); });
this.page.main.on("click", ".btn-less", function() { this.page.main.on('click', '.btn-less', function() {
var docname = $(this).attr("data-docname"); let docname = $(this).attr('data-docname');
me.page.main.find("."+docname).parent().find('.document-id').show(); me.page.main.find('.' + docname).parent().find('.document-id').show();
me.page.main.find("."+docname).parent().find('.document-html').hide(); me.page.main.find('.' + docname).parent().find('.document-html').hide();
}); });
me.start = 0; me.start = 0;
me.page.main.on("click", ".btn-get-records", function(){ me.page.main.on('click', '.btn-get-records', function() {
get_documents(patient.get_value(), me); get_documents(patient.get_value(), me);
}); });
}; };
var get_documents = function(patient, me){ 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'),
df: {
fieldtype: 'MultiSelectList',
fieldname: 'document_type',
placeholder: __('Select Document Type'),
input_class: 'input-xs',
change: () => {
me.start = 0;
me.page.main.find('.patient_documents_list').html('');
get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value());
},
get_data: () => {
return document_types.map(document_type => {
return {
description: document_type,
value: document_type
};
});
},
}
});
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('');
get_documents(patient, me, doctype_filter.get_value(), selected_date_range);
}
}
},
parent: $('.date-filter')
});
date_range_field.refresh();
});
};
let get_documents = function(patient, me, document_types="", 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;
frappe.call({ frappe.call({
"method": "erpnext.healthcare.page.patient_history.patient_history.get_feed", 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed',
args: { args: filters,
name: patient, callback: function(r) {
start: me.start, let data = r.message;
page_length: 20 if (data.length) {
},
callback: function (r) {
var data = r.message;
if(data.length){
add_to_records(me, data); add_to_records(me, data);
}else{ } else {
me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>"); me.page.main.find('.patient_documents_list').append(`
me.page.main.find(".btn-get-records").hide(); <div class='text-muted' align='center'>
<br><br>${__('No more records..')}<br><br>
</div>`);
me.page.main.find('.btn-get-records').hide();
} }
} }
}); });
}; };
var add_to_records = function(me, data){ let add_to_records = function(me, data) {
var details = "<ul class='nav nav-pills nav-stacked'>"; let details = "<ul class='nav nav-pills nav-stacked'>";
var i; let i;
for(i=0; i<data.length; i++){ for (i=0; i<data.length; i++) {
if(data[i].reference_doctype){ if (data[i].reference_doctype) {
let label = ''; let label = '';
if(data[i].subject){ if (data[i].subject) {
label += "<br/>"+data[i].subject; label += "<br/>" + data[i].subject;
} }
data[i] = add_date_separator(data[i]); data[i] = add_date_separator(data[i]);
if(frappe.user_info(data[i].owner).image){
if (frappe.user_info(data[i].owner).image) {
data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image); data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image);
} } else {
else{
data[i].imgsrc = false; data[i].imgsrc = false;
} }
var time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
time_line_heading += data[i].reference_doctype + " - "+ data[i].reference_name; let time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
details += `<li data-toggle='pill' class='patient_doc_menu' time_line_heading += data[i].reference_doctype + " - " +
data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'> `<a onclick="frappe.set_route('Form', '${data[i].reference_doctype}', '${data[i].reference_name}');">
<div class='col-sm-12 d-flex border-bottom py-3'>`; ${data[i].reference_name}
if (data[i].imgsrc){ </a>`;
details += `<span class='mr-3'>
<img class='avtar' src='${data[i].imgsrc}' width='32' height='32'> details += `
</img> <li data-toggle='pill' class='patient_doc_menu'
</span>`; data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'>
}else{ <div class='col-sm-12 d-flex border-bottom py-3'>`;
details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'><div align='center' class='standard-image'
style='background-color: #fafbfc;'>${data[i].practitioner ? data[i].practitioner.charAt(0) : "U"}</div></span>`; if (data[i].imgsrc) {
details += `
<span class='mr-3'>
<img class='avtar' src='${data[i].imgsrc}' width='32' height='32'></img>
</span>`;
} else {
details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'>
<div align='center' class='standard-image' style='background-color: #fafbfc;'>
${data[i].practitioner ? data[i].practitioner.charAt(0) : 'U'}
</div>
</span>`;
} }
details += `<div class='d-flex flex-column width-full'> details += `<div class='d-flex flex-column width-full'>
<div> <div>
`+time_line_heading+` on `+time_line_heading+`
<span> <span>
${data[i].date_sep} ${data[i].date_sep}
</span> </span>
@ -156,133 +240,150 @@ var add_to_records = function(me, data){
</li>`; </li>`;
} }
} }
details += "</ul>";
me.page.main.find(".patient_documents_list").append(details); details += '</ul>';
me.page.main.find('.patient_documents_list').append(details);
me.start += data.length; me.start += data.length;
if(data.length===20){
if (data.length === 20) {
me.page.main.find(".btn-get-records").show(); me.page.main.find(".btn-get-records").show();
}else{ } else {
me.page.main.find(".btn-get-records").hide(); me.page.main.find(".btn-get-records").hide();
me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>"); me.page.main.find(".patient_documents_list").append(`
<div class='text-muted' align='center'>
<br><br>${__('No more records..')}<br><br>
</div>`);
} }
}; };
var add_date_separator = function(data) { let add_date_separator = function(data) {
var date = frappe.datetime.str_to_obj(data.creation); let date = frappe.datetime.str_to_obj(data.communication_date);
let pdate = '';
let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); if (diff < 1) {
if(diff < 1) { pdate = __('Today');
var pdate = 'Today'; } else if (diff < 2) {
} else if(diff < 2) { pdate = __('Yesterday');
pdate = 'Yesterday';
} else { } else {
pdate = frappe.datetime.global_date_format(date); pdate = __('on ') + frappe.datetime.global_date_format(date);
} }
data.date_sep = pdate; data.date_sep = pdate;
return data; return data;
}; };
var show_patient_info = function(patient, me){ let show_patient_info = function(patient, me) {
frappe.call({ frappe.call({
"method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail',
args: { args: {
patient: patient patient: patient
}, },
callback: function (r) { callback: function(r) {
var data = r.message; let data = r.message;
var details = ""; let details = '';
if(data.image){ if (data.image) {
details += "<div><img class='thumbnail' width=75% src='"+data.image+"'></div>"; details += `<div><img class='thumbnail' width=75% src='${data.image}'></div>`;
} }
details += "<b>" + data.patient_name +"</b><br>" + data.sex;
if(data.email) details += "<br>" + data.email;
if(data.mobile) details += "<br>" + data.mobile;
if(data.occupation) details += "<br><br><b>Occupation :</b> " + data.occupation;
if(data.blood_group) details += "<br><b>Blood group : </b> " + data.blood_group;
if(data.allergies) details += "<br><br><b>Allergies : </b> "+ data.allergies.replace("\n", "<br>");
if(data.medication) details += "<br><b>Medication : </b> "+ data.medication.replace("\n", "<br>");
if(data.alcohol_current_use) details += "<br><br><b>Alcohol use : </b> "+ data.alcohol_current_use;
if(data.alcohol_past_use) details += "<br><b>Alcohol past use : </b> "+ data.alcohol_past_use;
if(data.tobacco_current_use) details += "<br><b>Tobacco use : </b> "+ data.tobacco_current_use;
if(data.tobacco_past_use) details += "<br><b>Tobacco past use : </b> "+ data.tobacco_past_use;
if(data.medical_history) details += "<br><br><b>Medical history : </b> "+ data.medical_history.replace("\n", "<br>");
if(data.surgical_history) details += "<br><b>Surgical history : </b> "+ data.surgical_history.replace("\n", "<br>");
if(data.surrounding_factors) details += "<br><br><b>Occupational hazards : </b> "+ data.surrounding_factors.replace("\n", "<br>");
if(data.other_risk_factors) details += "<br><b>Other risk factors : </b> " + data.other_risk_factors.replace("\n", "<br>");
if(data.patient_details) details += "<br><br><b>More info : </b> " + data.patient_details.replace("\n", "<br>");
if(details){ details += `<b> ${data.patient_name} </b><br> ${data.sex}`;
details = "<div style='padding-left:10px; font-size:13px;' align='center'>" + details + "</div>"; if (data.email) details += `<br> ${data.email}`;
if (data.mobile) details += `<br> ${data.mobile}`;
if (data.occupation) details += `<br><br><b> ${__('Occupation')} : </b> ${data.occupation}`;
if (data.blood_group) details += `<br><b> ${__('Blood Group')} : </b> ${data.blood_group}`;
if (data.allergies) details += `<br><br><b> ${__('Allerigies')} : </b> ${data.allergies.replace("\n", ", ")}`;
if (data.medication) details += `<br><b> ${__('Medication')} : </b> ${data.medication.replace("\n", ", ")}`;
if (data.alcohol_current_use) details += `<br><br><b> ${__('Alcohol use')} : </b> ${data.alcohol_current_use}`;
if (data.alcohol_past_use) details += `<br><b> ${__('Alcohol past use')} : </b> ${data.alcohol_past_use}`;
if (data.tobacco_current_use) details += `<br><b> ${__('Tobacco use')} : </b> ${data.tobacco_current_use}`;
if (data.tobacco_past_use) details += `<br><b> ${__('Tobacco past use')} : </b> ${data.tobacco_past_use}`;
if (data.medical_history) details += `<br><br><b> ${__('Medical history')} : </b> ${data.medical_history.replace("\n", ", ")}`;
if (data.surgical_history) details += `<br><b> ${__('Surgical history')} : </b> ${data.surgical_history.replace("\n", ", ")}`;
if (data.surrounding_factors) details += `<br><br><b> ${__('Occupational hazards')} : </b> ${data.surrounding_factors.replace("\n", ", ")}`;
if (data.other_risk_factors) details += `<br><b> ${__('Other risk factors')} : </b> ${data.other_risk_factors.replace("\n", ", ")}`;
if (data.patient_details) details += `<br><br><b> ${__('More info')} : </b> ${data.patient_details.replace("\n", ", ")}`;
if (details) {
details = `<div style='padding-left:10px; font-size:13px;' align='left'>` + details + `</div>`;
} }
me.page.main.find(".patient_details").html(details); me.page.main.find('.patient_details').html(details);
} }
}); });
}; };
var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
frappe.call({ frappe.call({
method: "erpnext.healthcare.utils.get_patient_vitals", method: 'erpnext.healthcare.utils.get_patient_vitals',
args:{ args:{
patient: patient patient: patient
}, },
callback: function(r) { callback: function(r) {
if (r.message){ if (r.message) {
var show_chart_btns_html = "<div style='padding-top:5px;'><a class='btn btn-default btn-xs btn-show-chart' \ let show_chart_btns_html = `
data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>Blood Pressure</a>\ <div style='padding-top:10px;'>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' \ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>
data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>Respiratory/Pulse Rate</a>\ ${__('Blood Pressure')}
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' \ </a>
data-pts='°C or °F' data-title='Temperature'>Temperature</a>\ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' \ ${__('Respiratory/Pulse Rate')}
data-pts='' data-title='BMI'>BMI</a></div>"; </a>
me.page.main.find(".show_chart_btns").html(show_chart_btns_html); <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' data-pts='°C or °F' data-title='Temperature'>
var data = r.message; ${__('Temperature')}
</a>
<a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' data-pts='' data-title='BMI'>
${__('BMI')}
</a>
</div>`;
me.page.main.find('.show_chart_btns').html(show_chart_btns_html);
let data = r.message;
let labels = [], datasets = []; let labels = [], datasets = [];
let bp_systolic = [], bp_diastolic = [], temperature = []; let bp_systolic = [], bp_diastolic = [], temperature = [];
let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = [];
for(var i=0; i<data.length; i++){
labels.push(data[i].signs_date+"||"+data[i].signs_time); for (let i=0; i<data.length; i++) {
if(btn_show_id=="bp"){ labels.push(data[i].signs_date+'||'+data[i].signs_time);
if (btn_show_id === 'bp') {
bp_systolic.push(data[i].bp_systolic); bp_systolic.push(data[i].bp_systolic);
bp_diastolic.push(data[i].bp_diastolic); bp_diastolic.push(data[i].bp_diastolic);
} }
if(btn_show_id=="temperature"){ if (btn_show_id === 'temperature') {
temperature.push(data[i].temperature); temperature.push(data[i].temperature);
} }
if(btn_show_id=="pulse_rate"){ if (btn_show_id === 'pulse_rate') {
pulse.push(data[i].pulse); pulse.push(data[i].pulse);
respiratory_rate.push(data[i].respiratory_rate); respiratory_rate.push(data[i].respiratory_rate);
} }
if(btn_show_id=="bmi"){ if (btn_show_id === 'bmi') {
bmi.push(data[i].bmi); bmi.push(data[i].bmi);
height.push(data[i].height); height.push(data[i].height);
weight.push(data[i].weight); weight.push(data[i].weight);
} }
} }
if(btn_show_id=="temperature"){ if (btn_show_id === 'temperature') {
datasets.push({name: "Temperature", values: temperature, chartType:'line'}); datasets.push({name: 'Temperature', values: temperature, chartType: 'line'});
} }
if(btn_show_id=="bmi"){ if (btn_show_id === 'bmi') {
datasets.push({name: "BMI", values: bmi, chartType:'line'}); datasets.push({name: 'BMI', values: bmi, chartType: 'line'});
datasets.push({name: "Height", values: height, chartType:'line'}); datasets.push({name: 'Height', values: height, chartType: 'line'});
datasets.push({name: "Weight", values: weight, chartType:'line'}); datasets.push({name: 'Weight', values: weight, chartType: 'line'});
} }
if(btn_show_id=="bp"){ if (btn_show_id === 'bp') {
datasets.push({name: "BP Systolic", values: bp_systolic, chartType:'line'}); datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'});
datasets.push({name: "BP Diastolic", values: bp_diastolic, chartType:'line'}); datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'});
} }
if(btn_show_id=="pulse_rate"){ if (btn_show_id === 'pulse_rate') {
datasets.push({name: "Heart Rate / Pulse", values: pulse, chartType:'line'}); datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'});
datasets.push({name: "Respiratory Rate", values: respiratory_rate, chartType:'line'}); datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'});
} }
new frappe.Chart( ".patient_vital_charts", { new frappe.Chart('.patient_vital_charts', {
data: { data: {
labels: labels, labels: labels,
datasets: datasets datasets: datasets
}, },
title: title, title: title,
type: 'axis-mixed', // 'axis-mixed', 'bar', 'line', 'pie', 'percentage' type: 'axis-mixed',
height: 200, height: 200,
colors: ['purple', '#ffa3ef', 'light-blue'], colors: ['purple', '#ffa3ef', 'light-blue'],
@ -291,9 +392,11 @@ var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
formatTooltipY: d => d + ' ' + pts, formatTooltipY: d => d + ' ' + pts,
} }
}); });
}else{ me.page.main.find('.header-separator').show();
me.page.main.find(".patient_vital_charts").html(""); } else {
me.page.main.find(".show_chart_btns").html(""); me.page.main.find('.patient_vital_charts').html('');
me.page.main.find('.show_chart_btns').html('');
me.page.main.find('.header-separator').hide();
} }
} }
}); });

View File

@ -4,36 +4,70 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import json
from frappe.utils import cint from frappe.utils import cint
from erpnext.healthcare.utils import render_docs_as_html from erpnext.healthcare.utils import render_docs_as_html
@frappe.whitelist() @frappe.whitelist()
def get_feed(name, start=0, page_length=20): def get_feed(name, document_types=None, date_range=None, start=0, page_length=20):
"""get feed""" """get feed"""
result = frappe.db.sql("""select name, owner, creation, filters = get_filters(name, document_types, date_range)
reference_doctype, reference_name, subject
from `tabPatient Medical Record` result = frappe.db.get_all('Patient Medical Record',
where patient=%(patient)s fields=['name', 'owner', 'communication_date',
order by creation desc 'reference_doctype', 'reference_name', 'subject'],
limit %(start)s, %(page_length)s""", filters=filters,
{ order_by='communication_date DESC',
"patient": name, limit=cint(page_length),
"start": cint(start), start=cint(start)
"page_length": cint(page_length) )
}, as_dict=True)
return result return result
def get_filters(name, document_types=None, date_range=None):
filters = {'patient': name}
if document_types:
document_types = json.loads(document_types)
if len(document_types):
filters['reference_doctype'] = ['IN', document_types]
if date_range:
try:
date_range = json.loads(date_range)
if date_range:
filters['communication_date'] = ['between', [date_range[0], date_range[1]]]
except json.decoder.JSONDecodeError:
pass
return filters
@frappe.whitelist() @frappe.whitelist()
def get_feed_for_dt(doctype, docname): def get_feed_for_dt(doctype, docname):
"""get feed""" """get feed"""
result = frappe.db.sql("""select name, owner, modified, creation, result = frappe.db.get_all('Patient Medical Record',
reference_doctype, reference_name, subject fields=['name', 'owner', 'communication_date',
from `tabPatient Medical Record` 'reference_doctype', 'reference_name', 'subject'],
where reference_name=%(docname)s and reference_doctype=%(doctype)s filters={
order by creation desc""", 'reference_doctype': doctype,
{ 'reference_name': docname
"docname": docname, },
"doctype": doctype order_by='communication_date DESC'
}, as_dict=True) )
return result return result
@frappe.whitelist()
def get_patient_history_doctypes():
document_types = []
settings = frappe.get_single("Patient History Settings")
for entry in settings.standard_doctypes:
document_types.append(entry.document_type)
for entry in settings.custom_doctypes:
document_types.append(entry.document_type)
return document_types

View File

@ -16,6 +16,7 @@ def setup_healthcare():
create_healthcare_item_groups() create_healthcare_item_groups()
create_sensitivity() create_sensitivity()
add_healthcare_service_unit_tree_root() add_healthcare_service_unit_tree_root()
setup_patient_history_settings()
def create_medical_departments(): def create_medical_departments():
departments = [ departments = [
@ -213,3 +214,82 @@ def get_company():
if company: if company:
return company[0].name return company[0].name
return None return None
def setup_patient_history_settings():
import json
settings = frappe.get_single('Patient History Settings')
configuration = get_patient_history_config()
for dt, config in configuration.items():
settings.append("standard_doctypes", {
"document_type": dt,
"date_fieldname": config[0],
"selected_fields": json.dumps(config[1])
})
settings.save()
def get_patient_history_config():
return {
"Patient Encounter": ("encounter_date", [
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"},
{"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"},
{"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"},
{"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"},
{"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"},
{"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"},
{"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"}
]),
"Clinical Procedure": ("start_date", [
{"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"},
{"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"},
{"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
{"label": "Sample", "fieldname": "sample", "fieldtype": "Link"}
]),
"Lab Test": ("result_date", [
{"label": "Test Template", "fieldname": "template", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"},
{"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"},
{"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"},
{"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"},
{"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"},
{"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"},
{"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"},
{"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"}
]),
"Therapy Session": ("start_date", [
{"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"},
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"},
{"label": "Duration", "fieldname": "duration", "fieldtype": "Int"},
{"label": "Location", "fieldname": "location", "fieldtype": "Link"},
{"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"},
{"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
{"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"},
{"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"},
{"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"}
]),
"Vital Signs": ("signs_date", [
{"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"},
{"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"},
{"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"},
{"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"},
{"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"},
{"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"},
{"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"},
{"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"},
{"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"},
{"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"},
{"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"}
]),
"Inpatient Medication Order": ("start_date", [
{"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
{"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"},
{"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"},
{"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"},
{"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"}
])
}

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import math import math
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded from frappe.utils import time_diff_in_hours, rounded
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
@ -77,11 +78,13 @@ def get_appointments_to_invoice(patient, company):
def get_encounters_to_invoice(patient, company): def get_encounters_to_invoice(patient, company):
if not isinstance(patient, str):
patient = patient.name
encounters_to_invoice = [] encounters_to_invoice = []
encounters = frappe.get_list( encounters = frappe.get_list(
'Patient Encounter', 'Patient Encounter',
fields=['*'], fields=['*'],
filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1}
) )
if encounters: if encounters:
for encounter in encounters: for encounter in encounters:
@ -90,6 +93,10 @@ def get_encounters_to_invoice(patient, company):
income_account = None income_account = None
service_item = None service_item = None
if encounter.practitioner: if encounter.practitioner:
if encounter.inpatient_record and \
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue
service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter) service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
income_account = get_income_account(encounter.practitioner, encounter.company) income_account = get_income_account(encounter.practitioner, encounter.company)
@ -642,11 +649,15 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
html += "<table class='table table-condensed table-bordered'>" \ html += "<table class='table table-condensed table-bordered'>" \
+ table_head + table_row + "</table>" + table_head + table_row + "</table>"
continue continue
#on other field types add label and value to html #on other field types add label and value to html
if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields:
html += '<br>{0} : {1}'.format(df.label or df.fieldname, \ if doc.get(df.fieldname):
doc.get(df.fieldname)) formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc)
html += '<br>{0} : {1}'.format(df.label or df.fieldname, formatted_value)
if not has_data : has_data = True if not has_data : has_data = True
if sec_on and col_on and has_data: if sec_on and col_on and has_data:
doc_html += section_html + html + '</div></div>' doc_html += section_html + html + '</div></div>'
elif sec_on and not col_on and has_data: elif sec_on and not col_on and has_data:

Some files were not shown because too many files have changed in this diff Show More