diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index 9336fc3706..57baac7681 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -51,6 +51,7 @@ frappe.ui.form.on('POS Closing Entry', {
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
+ pos_profile: frm.doc.pos_profile,
user: frm.doc.user
},
callback: (r) => {
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index 9899219bdc..2b91c74ce6 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -14,19 +14,51 @@ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import
class POSClosingEntry(Document):
def validate(self):
- user = frappe.get_all('POS Closing Entry',
- filters = { 'user': self.user, 'docstatus': 1 },
+ if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
+ frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
+
+ self.validate_pos_closing()
+ self.validate_pos_invoices()
+
+ def validate_pos_closing(self):
+ user = frappe.get_all("POS Closing Entry",
+ filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile },
or_filters = {
- 'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
- 'period_end_date': ('between', [self.period_start_date, self.period_end_date])
+ "period_start_date": ("between", [self.period_start_date, self.period_end_date]),
+ "period_end_date": ("between", [self.period_start_date, self.period_end_date])
})
if user:
- frappe.throw(_("POS Closing Entry {} against {} between selected period"
- .format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
+ bold_already_exists = frappe.bold(_("already exists"))
+ bold_user = frappe.bold(self.user)
+ frappe.throw(_("POS Closing Entry {} against {} between selected period")
+ .format(bold_already_exists, bold_user), title=_("Invalid Period"))
+
+ def validate_pos_invoices(self):
+ invalid_rows = []
+ for d in self.pos_transactions:
+ invalid_row = {'idx': d.idx}
+ pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
+ ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
+ if pos_invoice.consolidated_invoice:
+ invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
+ invalid_rows.append(invalid_row)
+ continue
+ if pos_invoice.pos_profile != self.pos_profile:
+ invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)))
+ if pos_invoice.docstatus != 1:
+ invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted")))
+ if pos_invoice.owner != self.user:
+ invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)))
- if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
- frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
+ if invalid_row.get('msg'):
+ invalid_rows.append(invalid_row)
+
+ if not invalid_rows:
+ return
+
+ error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows]
+ frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
def on_submit(self):
merge_pos_invoices(self.pos_transactions)
@@ -47,16 +79,15 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
return [c['user'] for c in cashiers_list]
@frappe.whitelist()
-def get_pos_invoices(start, end, user):
+def get_pos_invoices(start, end, pos_profile, user):
data = frappe.db.sql("""
select
name, timestamp(posting_date, posting_time) as "timestamp"
from
`tabPOS Invoice`
where
- owner = %s and docstatus = 1 and
- (consolidated_invoice is NULL or consolidated_invoice = '')
- """, (user), as_dict=1)
+ owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
+ """, (user, pos_profile), as_dict=1)
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
# need to get taxes and payments so can't avoid get_doc
@@ -76,7 +107,8 @@ def make_closing_entry_from_opening(opening_entry):
closing_entry.net_total = 0
closing_entry.total_quantity = 0
- invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user)
+ invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date,
+ closing_entry.pos_profile, closing_entry.user)
pos_transactions = []
taxes = []
diff --git a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
index 798637a840..6e7768dc54 100644
--- a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
+++ b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
@@ -7,8 +7,8 @@
"field_order": [
"mode_of_payment",
"opening_amount",
- "closing_amount",
"expected_amount",
+ "closing_amount",
"difference"
],
"fields": [
@@ -26,8 +26,7 @@
"in_list_view": 1,
"label": "Expected Amount",
"options": "company:company_currency",
- "read_only": 1,
- "reqd": 1
+ "read_only": 1
},
{
"fieldname": "difference",
@@ -55,9 +54,10 @@
"reqd": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-29 15:03:34.533607",
+ "modified": "2020-10-23 16:45:43.662034",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry Detail",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index c43cb794aa..86062d1e7c 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -9,80 +9,63 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
this._super(doc);
},
- onload() {
+ onload(doc) {
this._super();
- if(this.frm.doc.__islocal && this.frm.doc.is_pos) {
- //Load pos profile data on the invoice if the default value of Is POS is 1
-
- me.frm.script_manager.trigger("is_pos");
- me.frm.refresh_fields();
+ if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
+ this.frm.script_manager.trigger("is_pos");
+ this.frm.refresh_fields();
}
},
refresh(doc) {
this._super();
if (doc.docstatus == 1 && !doc.is_return) {
- if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
- cur_frm.add_custom_button(__('Return'),
- this.make_sales_return, __('Create'));
- cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
- }
+ this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create'));
+ this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
- if (this.frm.doc.is_return) {
+ if (doc.is_return && doc.__islocal) {
this.frm.return_print_format = "Sales Invoice Return";
- cur_frm.set_value('consolidated_invoice', '');
+ this.frm.set_value('consolidated_invoice', '');
}
},
- is_pos: function(frm){
+ is_pos: function() {
this.set_pos_data();
},
- set_pos_data: function() {
+ set_pos_data: async function() {
if(this.frm.doc.is_pos) {
this.frm.set_value("allocate_advances_automatically", 0);
if(!this.frm.doc.company) {
this.frm.set_value("is_pos", 0);
frappe.msgprint(__("Please specify Company to proceed"));
} else {
- var me = this;
- return this.frm.call({
- doc: me.frm.doc,
+ const r = await this.frm.call({
+ doc: this.frm.doc,
method: "set_missing_values",
- callback: function(r) {
- if(!r.exc) {
- if(r.message) {
- me.frm.pos_print_format = r.message.print_format || "";
- me.frm.meta.default_print_format = r.message.print_format || "";
- me.frm.allow_edit_rate = r.message.allow_edit_rate;
- me.frm.allow_edit_discount = r.message.allow_edit_discount;
- me.frm.doc.campaign = r.message.campaign;
- me.frm.allow_print_before_pay = r.message.allow_print_before_pay;
- }
- me.frm.script_manager.trigger("update_stock");
- me.calculate_taxes_and_totals();
- if(me.frm.doc.taxes_and_charges) {
- me.frm.script_manager.trigger("taxes_and_charges");
- }
- frappe.model.set_default_values(me.frm.doc);
- me.set_dynamic_labels();
-
- }
- }
+ freeze: true
});
+ if(!r.exc) {
+ if(r.message) {
+ this.frm.pos_print_format = r.message.print_format || "";
+ this.frm.meta.default_print_format = r.message.print_format || "";
+ this.frm.doc.campaign = r.message.campaign;
+ this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
+ }
+ this.frm.script_manager.trigger("update_stock");
+ this.calculate_taxes_and_totals();
+ this.frm.doc.taxes_and_charges && this.frm.script_manager.trigger("taxes_and_charges");
+ frappe.model.set_default_values(this.frm.doc);
+ this.set_dynamic_labels();
+ }
}
}
- else this.frm.trigger("refresh");
},
customer() {
if (!this.frm.doc.customer) return
-
- if (this.frm.doc.is_pos){
- var pos_profile = this.frm.doc.pos_profile;
- }
- var me = this;
+ const pos_profile = this.frm.doc.pos_profile;
if(this.frm.updating_party_details) return;
erpnext.utils.get_party_details(this.frm,
"erpnext.accounts.party.get_party_details", {
@@ -92,8 +75,8 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile
- }, function() {
- me.apply_pricing_rule();
+ }, () => {
+ this.apply_pricing_rule();
});
},
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 5b0822e323..b0a7547ce8 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -10,12 +10,10 @@ from erpnext.controllers.selling_controller import SellingController
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date
-from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
- get_loyalty_program_details_with_points, validate_loyalty_points
-
-from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
-from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
+from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
+from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option, get_mode_of_payment_info
from six import iteritems
@@ -30,8 +28,7 @@ class POSInvoice(SalesInvoice):
# run on validate method of selling controller
super(SalesInvoice, self).validate()
self.validate_auto_set_posting_time()
- self.validate_pos_paid_amount()
- self.validate_pos_return()
+ self.validate_mode_of_payment()
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty")
self.validate_debit_to_acc()
@@ -41,11 +38,11 @@ class POSInvoice(SalesInvoice):
self.validate_item_cost_centers()
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
- self.validate_return_items()
+ self.validate_return_items_qty()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
- self.verify_payment_amount()
+ self.validate_payment_amount()
self.validate_loyalty_transaction()
def on_submit(self):
@@ -84,70 +81,98 @@ class POSInvoice(SalesInvoice):
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_stock_availablility(self):
- allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
+ if self.is_return:
+ return
+ allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
+ error_msg = []
for d in self.get('items'):
+ msg = ""
if d.serial_no:
- filters = {
- "item_code": d.item_code,
- "warehouse": d.warehouse,
- "delivery_document_no": "",
- "sales_invoice": ""
- }
+ filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
- reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters)
- serial_nos = d.serial_no.split("\n")
- serial_nos = ' '.join(serial_nos).split() # remove whitespaces
- invalid_serial_nos = []
- for s in serial_nos:
- if s in reserved_serial_nos:
- invalid_serial_nos.append(s)
- if len(invalid_serial_nos):
- multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
- frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. Please select valid serial no.").format(
- d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos))), title=_("Not Available"))
+ reserved_serial_nos = get_pos_reserved_serial_nos(filters)
+ serial_nos = get_serial_nos(d.serial_no)
+ invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
+
+ bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
+ if len(invalid_serial_nos) == 1:
+ msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
+ .format(d.idx, bold_invalid_serial_nos))
+ elif invalid_serial_nos:
+ msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
+ .format(d.idx, bold_invalid_serial_nos))
+
else:
if allow_negative_stock:
return
available_stock = get_stock_availability(d.item_code, d.warehouse)
- if not (flt(available_stock) > 0):
- frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.').format(
- d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse)), title=_("Not Available"))
+ item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
+ if flt(available_stock) <= 0:
+ msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
elif flt(available_stock) < flt(d.qty):
- frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.').format(
- d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)), title=_("Not Available"))
+ msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
+ .format(d.idx, item_code, warehouse, qty))
+ if msg:
+ error_msg.append(msg)
+
+ if error_msg:
+ frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
def validate_serialised_or_batched_item(self):
+ error_msg = []
for d in self.get("items"):
serialized = d.get("has_serial_no")
batched = d.get("has_batch_no")
no_serial_selected = not d.get("serial_no")
no_batch_selected = not d.get("batch_no")
-
+ msg = ""
+ item_code = frappe.bold(d.item_code)
if serialized and batched and (no_batch_selected or no_serial_selected):
- frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.').format(
- d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
+ msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
+ .format(d.idx, item_code))
if serialized and no_serial_selected:
- frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.').format(
- d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
+ msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
+ .format(d.idx, item_code))
if batched and no_batch_selected:
- frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.').format(
- d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
+ msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
+ .format(d.idx, item_code))
+ if msg:
+ error_msg.append(msg)
- def validate_return_items(self):
+ if error_msg:
+ frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
+
+ def validate_return_items_qty(self):
if not self.get("is_return"): return
for d in self.get("items"):
if d.get("qty") > 0:
- frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
- .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
+ frappe.throw(
+ _("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
+ .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")
+ )
+ if d.get("serial_no"):
+ serial_nos = get_serial_nos(d.serial_no)
+ for sr in serial_nos:
+ serial_no_exists = frappe.db.exists("POS Invoice Item", {
+ "parent": self.return_against,
+ "serial_no": ["like", d.get("serial_no")]
+ })
+ if not serial_no_exists:
+ bold_return_against = frappe.bold(self.return_against)
+ bold_serial_no = frappe.bold(sr)
+ frappe.throw(
+ _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
+ .format(d.idx, bold_serial_no, bold_return_against)
+ )
- def validate_pos_paid_amount(self):
- if len(self.payments) == 0 and self.is_pos:
+ def validate_mode_of_payment(self):
+ if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def validate_change_account(self):
@@ -165,20 +190,18 @@ class POSInvoice(SalesInvoice):
if flt(self.change_amount) and not self.account_for_change_amount:
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
- def verify_payment_amount(self):
+ def validate_payment_amount(self):
+ total_amount_in_payments = 0
for entry in self.payments:
+ total_amount_in_payments += entry.amount
if not self.is_return and entry.amount < 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
- def validate_pos_return(self):
- if self.is_pos and self.is_return:
- total_amount_in_payments = 0
- for payment in self.payments:
- total_amount_in_payments += payment.amount
+ if self.is_return:
invoice_total = self.rounded_total or self.grand_total
- if total_amount_in_payments < invoice_total:
+ if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_loyalty_transaction(self):
@@ -233,55 +256,45 @@ class POSInvoice(SalesInvoice):
pos_profile = get_pos_profile(self.company) or {}
self.pos_profile = pos_profile.get('name')
- pos = {}
+ profile = {}
if self.pos_profile:
- pos = frappe.get_doc('POS Profile', self.pos_profile)
+ profile = frappe.get_doc('POS Profile', self.pos_profile)
if not self.get('payments') and not for_validate:
- update_multi_mode_option(self, pos)
-
- if not self.account_for_change_amount:
- self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
-
- if pos:
- if not for_validate:
- self.tax_category = pos.get("tax_category")
+ update_multi_mode_option(self, profile)
+
+ if self.is_return and not for_validate:
+ add_return_modes(self, profile)
+ if profile:
if not for_validate and not self.customer:
- self.customer = pos.customer
+ self.customer = profile.customer
- self.ignore_pricing_rule = pos.ignore_pricing_rule
- if pos.get('account_for_change_amount'):
- self.account_for_change_amount = pos.get('account_for_change_amount')
- if pos.get('warehouse'):
- self.set_warehouse = pos.get('warehouse')
+ self.ignore_pricing_rule = profile.ignore_pricing_rule
+ self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
+ self.set_warehouse = profile.get('warehouse') or self.set_warehouse
- for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
+ for fieldname in ('currency', 'letter_head', 'tc_name',
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
- 'write_off_cost_center', 'apply_discount_on', 'cost_center'):
- if (not for_validate) or (for_validate and not self.get(fieldname)):
- self.set(fieldname, pos.get(fieldname))
-
- if pos.get("company_address"):
- self.company_address = pos.get("company_address")
+ 'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
+ 'ignore_pricing_rule', 'company_address', 'update_stock'):
+ if not for_validate:
+ self.set(fieldname, profile.get(fieldname))
if self.customer:
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
- selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
+ selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
else:
- selling_price_list = pos.get('selling_price_list')
+ selling_price_list = profile.get('selling_price_list')
if selling_price_list:
self.set('selling_price_list', selling_price_list)
- if not for_validate:
- self.update_stock = cint(pos.get("update_stock"))
-
# set pos values in items
for item in self.get("items"):
if item.get('item_code'):
- profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
+ profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile)
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@@ -294,10 +307,13 @@ class POSInvoice(SalesInvoice):
if self.taxes_and_charges and not len(self.get("taxes")):
self.set_taxes()
- return pos
+ if not self.account_for_change_amount:
+ self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
+
+ return profile
def set_missing_values(self, for_validate=False):
- pos = self.set_pos_fields(for_validate)
+ profile = self.set_pos_fields(for_validate)
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
@@ -307,17 +323,15 @@ class POSInvoice(SalesInvoice):
super(SalesInvoice, self).set_missing_values(for_validate)
- print_format = pos.get("print_format") if pos else None
+ print_format = profile.get("print_format") if profile else None
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
print_format = 'POS Invoice'
- if pos:
+ if profile:
return {
"print_format": print_format,
- "allow_edit_rate": pos.get("allow_user_to_edit_rate"),
- "allow_edit_discount": pos.get("allow_user_to_edit_discount"),
- "campaign": pos.get("campaign"),
- "allow_print_before_pay": pos.get("allow_print_before_pay")
+ "campaign": profile.get("campaign"),
+ "allow_print_before_pay": profile.get("allow_print_before_pay")
}
def set_account_for_mode_of_payment(self):
@@ -373,11 +387,9 @@ def get_stock_availability(item_code, warehouse):
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
- if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
+ if sle_qty and pos_sales_qty:
return sle_qty - pos_sales_qty
else:
- # when sle_qty is 0
- # when sle_qty > 0 and pos_sales_qty is 0
return sle_qty
@frappe.whitelist()
@@ -410,4 +422,19 @@ def make_merge_log(invoices):
})
if merge_log.get('pos_invoices'):
- return merge_log.as_dict()
\ No newline at end of file
+ return merge_log.as_dict()
+
+def add_return_modes(doc, pos_profile):
+ def append_payment(payment_mode):
+ payment = doc.append('payments', {})
+ payment.default = payment_mode.default
+ payment.mode_of_payment = payment_mode.parent
+ payment.account = payment_mode.default_account
+ payment.type = payment_mode.type
+
+ for pos_payment_method in pos_profile.get('payments'):
+ pos_payment_method = pos_payment_method.as_dict()
+ mode_of_payment = pos_payment_method.mode_of_payment
+ if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
+ payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
+ append_payment(payment_mode[0])
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 3a229b1787..add27e9dff 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -26,18 +26,25 @@ class POSInvoiceMergeLog(Document):
for d in self.pos_invoices:
status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
-
+
+ bold_pos_invoice = frappe.bold(d.pos_invoice)
+ bold_status = frappe.bold(status)
if docstatus != 1:
- frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
+ frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
if status == "Consolidated":
- frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
- if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated":
- # if return entry is not getting merged in the current pos closing and if it is not consolidated
- frappe.throw(
- _("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \
- You can add original invoice {} manually to proceed.")
- .format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against))
- )
+ frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status))
+ if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]:
+ bold_return_against = frappe.bold(return_against)
+ return_against_status = frappe.db.get_value('POS Invoice', return_against, "status")
+ if return_against_status != "Consolidated":
+ # if return entry is not getting merged in the current pos closing and if it is not consolidated
+ bold_unconsolidated = frappe.bold("not Consolidated")
+ msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
+ .format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
+ msg += _("Original invoice should be consolidated before or along with the return invoice.")
+ msg += "
"
+ msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
+ frappe.throw(msg)
def on_submit(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
index b9e07b8030..acac1c4072 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
@@ -17,18 +17,25 @@ class POSOpeningEntry(StatusUpdater):
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
- frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company)))
+ frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company))
if not cint(frappe.db.get_value("User", self.user, "enabled")):
- frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user)))
+ frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
def validate_payment_method_account(self):
+ invalid_modes = []
for d in self.balance_details:
account = frappe.db.get_value("Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
if not account:
- frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
- .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
+ invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
+
+ if invalid_modes:
+ if invalid_modes == 1:
+ msg = _("Please set default Cash or Bank account in Mode of Payment {}")
+ else:
+ msg = _("Please set default Cash or Bank account in Mode of Payments {}")
+ frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def on_submit(self):
self.set_status(update=True)
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json
index 4d5e1eb798..30ebd307c4 100644
--- a/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json
+++ b/erpnext/accounts/doctype/pos_payment_method/pos_payment_method.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"default",
+ "allow_in_returns",
"mode_of_payment"
],
"fields": [
@@ -24,11 +25,19 @@
"label": "Mode of Payment",
"options": "Mode of Payment",
"reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_returns",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Allow In Returns"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-29 15:08:41.704844",
+ "modified": "2020-10-20 12:58:46.114456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Payment Method",
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 999da75997..4e22218c6e 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -290,28 +290,30 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
- "mandatory_depends_on": "update_stock",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
- "options": "Warehouse"
- },
- {
- "default": "0",
- "fieldname": "update_stock",
- "fieldtype": "Check",
- "label": "Update Stock"
+ "options": "Warehouse",
+ "reqd": 1
},
{
"default": "0",
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule"
+ },
+ {
+ "default": "1",
+ "fieldname": "update_stock",
+ "fieldtype": "Check",
+ "label": "Update Stock",
+ "read_only": 1
}
],
"icon": "icon-cog",
"idx": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-01 17:29:27.759088",
+ "modified": "2020-10-20 13:16:50.665081",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index 1d160a5aa7..ee76bba750 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -56,19 +56,29 @@ class POSProfile(Document):
if not self.payments:
frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
- default_mode_of_payment = [d.default for d in self.payments if d.default]
- if not default_mode_of_payment:
+ default_mode = [d.default for d in self.payments if d.default]
+ if not default_mode:
frappe.throw(_("Please select a default mode of payment"))
- if len(default_mode_of_payment) > 1:
+ if len(default_mode) > 1:
frappe.throw(_("You can only select one mode of payment as default"))
+ invalid_modes = []
for d in self.payments:
- account = frappe.db.get_value("Mode of Payment Account",
- {"parent": d.mode_of_payment, "company": self.company}, "default_account")
+ account = frappe.db.get_value(
+ "Mode of Payment Account",
+ {"parent": d.mode_of_payment, "company": self.company},
+ "default_account"
+ )
if not account:
- frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
- .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
+ invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
+
+ if invalid_modes:
+ if invalid_modes == 1:
+ msg = _("Please set default Cash or Bank account in Mode of Payment {}")
+ else:
+ msg = _("Please set default Cash or Bank account in Mode of Payments {}")
+ frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def on_update(self):
self.set_defaults()
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js
index 05cb7f0b4b..8890d59403 100644
--- a/erpnext/accounts/doctype/pos_settings/pos_settings.js
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js
@@ -9,8 +9,7 @@ frappe.ui.form.on('POS Settings', {
get_invoice_fields: function(frm) {
frappe.model.with_doctype("POS Invoice", () => {
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
- if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
- ['Table', 'Button'].includes(d.fieldtype)) {
+ if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) {
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
} else {
return null;
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c5260a1239..91c4dfb587 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -151,14 +151,16 @@ class PurchaseInvoice(BuyingController):
["account_type", "report_type", "account_currency"], as_dict=True)
if account.report_type != "Balance Sheet":
- frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
- You can change the parent account to a Balance Sheet account or select a different account.")
- .format(frappe.bold("Credit To")), title=_("Invalid Account"))
+ frappe.throw(
+ _("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.")
+ .format(frappe.bold("Credit To")), title=_("Invalid Account")
+ )
if self.supplier and account.account_type != "Payable":
- frappe.throw(_("Please ensure {} account is a Payable account. \
- Change the account type to Payable or select a different account.")
- .format(frappe.bold("Credit To")), title=_("Invalid Account"))
+ frappe.throw(
+ _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
+ .format(frappe.bold("Credit To")), title=_("Invalid Account")
+ )
self.party_account_currency = account.account_currency
@@ -244,10 +246,10 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
- frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because account {2}
- is not linked to warehouse {3} or it is not the default inventory account'''.format(
- item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]),
- frappe.bold(item.expense_account), frappe.bold(item.warehouse))))
+ msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
+ msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
+ msg += _("or it is not the default inventory account")
+ frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
@@ -259,19 +261,19 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr:
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because
- expense is booked against this account in Purchase Receipt {2}'''.format(
- item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))))
+ msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
+ msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
+ frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase
- Receipt is created against Item {2}. This is done to handle accounting for cases
- when Purchase Receipt is created after Purchase Invoice'''.format(
- item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))))
+ msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
+ msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
+ msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
+ frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
@@ -299,10 +301,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'):
if not d.purchase_order:
- throw(_("""Purchase Order Required for item {0}
- To submit the invoice without purchase order please set
- {1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')),
- frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
+ msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
+ msg += "
"
+ msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
+ msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
stock_items = self.get_stock_items()
@@ -313,10 +316,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'):
if not d.purchase_receipt and d.item_code in stock_items:
- throw(_("""Purchase Receipt Required for item {0}
- To submit the invoice without purchase receipt please set
- {1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')),
- frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
+ msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
+ msg += "
"
+ msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
+ msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
if self.write_off_amount and not self.write_off_account:
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 801e688deb..4b598877d9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -479,14 +479,14 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet":
- frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
- You can change the parent account to a Balance Sheet account or select a different account.")
- .format(frappe.bold("Debit To")), title=_("Invalid Account"))
+ msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
+ msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
+ frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
- frappe.throw(_("Please ensure {} account is a Receivable account. \
- Change the account type to Receivable or select a different account.")
- .format(frappe.bold("Debit To")), title=_("Invalid Account"))
+ msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
+ msg += _("Change the account type to Receivable or select a different account.")
+ frappe.throw(msg, title=_("Invalid Account"))
self.party_account_currency = account.account_currency
@@ -1141,8 +1141,10 @@ class SalesInvoice(SellingController):
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
if against_lp_entry:
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
- frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed.
- First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
+ frappe.throw(
+ _('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
+ .format(self.doctype, self.doctype, invoice_list)
+ )
else:
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
# Set loyalty program
@@ -1613,17 +1615,25 @@ def update_multi_mode_option(doc, pos_profile):
payment.type = payment_mode.type
doc.set('payments', [])
+ invalid_modes = []
for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict()
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
if not payment_mode:
- frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
- .format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account"))
+ invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
+ continue
payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0])
+ if invalid_modes:
+ if invalid_modes == 1:
+ msg = _("Please set default Cash or Bank account in Mode of Payment {}")
+ else:
+ msg = _("Please set default Cash or Bank account in Mode of Payments {}")
+ frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
+
def get_all_mode_of_payments(doc):
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 1b65a318b6..7dfce85629 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -307,7 +307,7 @@ def get_accounts(company, root_type):
where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True)
-def filter_accounts(accounts, depth=10):
+def filter_accounts(accounts, depth=20):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 0cf3d24478..0181ae78b4 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -19,6 +19,8 @@ from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
+from erpnext.stock.doctype.batch.test_batch import make_new_batch
+from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@@ -686,7 +688,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code)
+ make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1)
@@ -708,7 +710,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code)
+ make_subcontracted_item(item_code=item_code)
make_item('Sub Contracted Raw Material 1', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
@@ -767,6 +769,129 @@ class TestPurchaseOrder(unittest.TestCase):
update_backflush_based_on("BOM")
+ def test_backflushed_based_on_for_multiple_batches(self):
+ item_code = "_Test Subcontracted FG Item 2"
+ make_item('Sub Contracted Raw Material 2', {
+ 'is_stock_item': 1,
+ 'is_sub_contracted_item': 1
+ })
+
+ make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
+ raw_materials=["Sub Contracted Raw Material 2"])
+
+ update_backflush_based_on("Material Transferred for Subcontract")
+
+ order_qty = 500
+ po = create_purchase_order(item_code=item_code, qty=order_qty,
+ is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
+
+ make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
+
+ rm_items = [
+ {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
+ "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
+
+ rm_item_string = json.dumps(rm_items)
+ se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
+ se.submit()
+
+ for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
+ make_new_batch(batch_id=batch, item_code=item_code)
+
+ pr = make_purchase_receipt(po.name)
+
+ # partial receipt
+ pr.get('items')[0].qty = 30
+ pr.get('items')[0].batch_no = "ABCD1"
+
+ purchase_order = po.name
+ purchase_order_item = po.items[0].name
+
+ for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
+ pr.append("items", {
+ "item_code": pr.get('items')[0].item_code,
+ "item_name": pr.get('items')[0].item_name,
+ "uom": pr.get('items')[0].uom,
+ "stock_uom": pr.get('items')[0].stock_uom,
+ "warehouse": pr.get('items')[0].warehouse,
+ "conversion_factor": pr.get('items')[0].conversion_factor,
+ "cost_center": pr.get('items')[0].cost_center,
+ "rate": pr.get('items')[0].rate,
+ "qty": qty,
+ "batch_no": batch_no,
+ "purchase_order": purchase_order,
+ "purchase_order_item": purchase_order_item
+ })
+
+ pr.submit()
+
+ pr1 = make_purchase_receipt(po.name)
+ pr1.get('items')[0].qty = 300
+ pr1.get('items')[0].batch_no = "ABCD1"
+ pr1.save()
+
+ pr_key = ("Sub Contracted Raw Material 2", po.name)
+ consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
+
+ self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
+ self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
+
+ update_backflush_based_on("BOM")
+
+ def test_supplied_qty_against_subcontracted_po(self):
+ item_code = "_Test Subcontracted FG Item 5"
+ make_item('Sub Contracted Raw Material 4', {
+ 'is_stock_item': 1,
+ 'is_sub_contracted_item': 1
+ })
+
+ make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
+
+ update_backflush_based_on("Material Transferred for Subcontract")
+
+ order_qty = 250
+ po = create_purchase_order(item_code=item_code, qty=order_qty,
+ is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True)
+
+ # Add same subcontracted items multiple times
+ po.append("items", {
+ "item_code": item_code,
+ "qty": order_qty,
+ "schedule_date": add_days(nowdate(), 1),
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ po.set_missing_values()
+ po.submit()
+
+ # Material receipt entry for the raw materials which will be send to supplier
+ make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100)
+
+ rm_items = [
+ {
+ "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
+ "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name
+ },
+ {
+ "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
+ "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name
+ },
+ ]
+
+ # Raw Materials transfer entry from stores to supplier's warehouse
+ rm_item_string = json.dumps(rm_items)
+ se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
+ se.submit()
+
+ po_doc = frappe.get_doc("Purchase Order", po.name)
+ for row in po_doc.supplied_items:
+ # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
+ self.assertEqual(row.supplied_qty, 250.0)
+
+ update_backflush_based_on("BOM")
+
def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
frappe.db.set_value("Accounts Settings", "Accounts Settings",
@@ -839,27 +964,33 @@ def make_pr_against_po(po, received_qty=0):
pr.submit()
return pr
-def make_subcontracted_item(item_code):
+def make_subcontracted_item(**args):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- if not frappe.db.exists('Item', item_code):
- make_item(item_code, {
+ args = frappe._dict(args)
+
+ if not frappe.db.exists('Item', args.item_code):
+ make_item(args.item_code, {
'is_stock_item': 1,
- 'is_sub_contracted_item': 1
+ 'is_sub_contracted_item': 1,
+ 'has_batch_no': args.get("has_batch_no") or 0
})
- if not frappe.db.exists('Item', "Test Extra Item 1"):
- make_item("Test Extra Item 1", {
- 'is_stock_item': 1,
- })
+ if not args.raw_materials:
+ if not frappe.db.exists('Item', "Test Extra Item 1"):
+ make_item("Test Extra Item 1", {
+ 'is_stock_item': 1,
+ })
- if not frappe.db.exists('Item', "Test Extra Item 2"):
- make_item("Test Extra Item 2", {
- 'is_stock_item': 1,
- })
+ if not frappe.db.exists('Item', "Test Extra Item 2"):
+ make_item("Test Extra Item 2", {
+ 'is_stock_item': 1,
+ })
- if not frappe.db.get_value('BOM', {'item': item_code}, 'name'):
- make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1'])
+ args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
+
+ if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
+ make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
def update_backflush_based_on(based_on):
doc = frappe.get_doc('Buying Settings')
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index fc32977658..983cfa8c15 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -946,8 +946,10 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c
company_currency = frappe.get_cached_value('Company', company, "default_currency")
if not conversion_rate:
- throw(_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format(
- conversion_rate_label, currency, company_currency))
+ throw(
+ _("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.")
+ .format(conversion_rate_label, currency, company_currency)
+ )
def validate_taxes_and_charges(tax):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index e05c70e41c..f376836f7b 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate
-
+from six import iteritems
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
@@ -112,8 +112,8 @@ class BuyingController(StockController):
"docstatus": 1
})]
if self.is_return and len(not_cancelled_asset):
- frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.".format(self.return_against)),
- title=_("Not Allowed"))
+ frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.")
+ .format(self.return_against), title=_("Not Allowed"))
def get_asset_items(self):
if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
@@ -298,10 +298,10 @@ class BuyingController(StockController):
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
- backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
+ # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
- rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order)
+ rm_item_key = (raw_material.rm_item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
@@ -330,8 +330,10 @@ class BuyingController(StockController):
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
+ backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
+
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
- qty, transferred_batch_qty_map, backflushed_batch_qty_map)
+ qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
@@ -343,6 +345,10 @@ class BuyingController(StockController):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
+ if not rm.main_item_code:
+ rm.main_item_code = fg_item_doc.item_code
+
+ rm.reference_name = fg_item_doc.name
rm.required_qty = qty
rm.consumed_qty = qty
@@ -792,8 +798,8 @@ class BuyingController(StockController):
asset.set(field, None)
asset.supplier = None
if asset.docstatus == 1 and delete_asset:
- frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\
- Please cancel the it to continue.').format(frappe.utils.get_link_to_form('Asset', asset.name)))
+ frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue.')
+ .format(frappe.utils.get_link_to_form('Asset', asset.name)))
asset.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_mandatory = True
@@ -873,7 +879,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
- AND sed.subcontracted_item = %s
+ AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
@@ -890,39 +896,49 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
- common_query = """
- SELECT
- CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
- SUM(prsi.consumed_qty) AS qty,
- {serial_no_concat_syntax} AS serial_nos,
- {batch_no_concat_syntax} AS batch_nos
- FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
- WHERE
- pr.name = pri.parent
- AND pr.name = prsi.parent
- AND pri.purchase_order IN %s
- AND pri.item_code = prsi.main_item_code
- AND pr.docstatus = 1
- GROUP BY prsi.rm_item_code, pri.purchase_order
- """
+ purchase_receipts = frappe.get_all("Purchase Receipt Item",
+ fields = ["purchase_order", "item_code", "name", "parent"],
+ filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
- backflushed_raw_materials = frappe.db.multisql({
- 'mariadb': common_query.format(
- serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
- batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
- ),
- 'postgres': common_query.format(
- serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
- batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
- )
- }, (purchase_orders, ), as_dict=1)
+ distinct_purchase_receipts = {}
+ for pr in purchase_receipts:
+ key = (pr.purchase_order, pr.item_code, pr.parent)
+ distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
- for item in backflushed_raw_materials:
- backflushed_raw_materials_map.setdefault(item.item_key, item)
+ for args, references in iteritems(distinct_purchase_receipts):
+ purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
+
+ for data in purchase_receipt_supplied_items:
+ pr_key = (data.rm_item_code, args[0])
+ if pr_key not in backflushed_raw_materials_map:
+ backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
+ "qty": 0.0,
+ "serial_no": [],
+ "batch_no": [],
+ "consumed_batch": {}
+ }))
+
+ row = backflushed_raw_materials_map.get(pr_key)
+ row.qty += data.consumed_qty
+
+ for field in ["serial_no", "batch_no"]:
+ if data.get(field):
+ row[field].append(data.get(field))
+
+ if data.get("batch_no"):
+ if data.get("batch_no") in row.consumed_batch:
+ row.consumed_batch[data.get("batch_no")] += data.consumed_qty
+ else:
+ row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
+def get_supplied_items(item_code, purchase_receipt, references):
+ return frappe.get_all("Purchase Receipt Item Supplied",
+ fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"],
+ filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
+
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@@ -1004,14 +1020,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
- sed.item_code
+ sed.item_code,
+ sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
- AND sed.subcontracted_item = %s
+ AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
@@ -1019,8 +1036,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
- transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
- transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
+ key = ((batch_data.item_code, fg_item)
+ if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
+ transferred_batch_qty_map.setdefault(key, {})
+ transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
@@ -1057,10 +1076,11 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item):
return backflushed_batch_qty_map
-def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map):
+def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
- backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
+ if not transferred_batches:
+ transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []
diff --git a/erpnext/domains/healthcare.py b/erpnext/domains/healthcare.py
index 8bd4c76290..bbeb2c66bc 100644
--- a/erpnext/domains/healthcare.py
+++ b/erpnext/domains/healthcare.py
@@ -49,6 +49,22 @@ data = {
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
'insert_after': 'reference_dt'
}
+ ],
+ 'Stock Entry': [
+ {
+ 'fieldname': 'inpatient_medication_entry', 'label': 'Inpatient Medication Entry', 'fieldtype': 'Link', 'options': 'Inpatient Medication Entry',
+ 'insert_after': 'credit_note', 'read_only': True
+ }
+ ],
+ 'Stock Entry Detail': [
+ {
+ 'fieldname': 'patient', 'label': 'Patient', 'fieldtype': 'Link', 'options': 'Patient',
+ 'insert_after': 'po_detail', 'read_only': True
+ },
+ {
+ 'fieldname': 'inpatient_medication_entry_child', 'label': 'Inpatient Medication Entry Child', 'fieldtype': 'Data',
+ 'insert_after': 'patient', 'read_only': True
+ }
]
},
'on_setup': 'erpnext.healthcare.setup.setup_healthcare'
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index bf9f2215f3..948e7cc1ae 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -7,7 +7,7 @@ import frappe
import json
from frappe import _
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import flt, cstr
+from frappe.utils import flt, cstr, getdate
from frappe.email.doctype.email_group.email_group import add_subscribers
def get_course(program):
@@ -67,6 +67,13 @@ def mark_attendance(students_present, students_absent, course_schedule=None, stu
:param date: Date.
"""
+ if student_group:
+ academic_year = frappe.db.get_value('Student Group', student_group, 'academic_year')
+ if academic_year:
+ year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date'])
+ if getdate(date) < getdate(year_start_date) or getdate(date) > getdate(year_end_date):
+ frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year))
+
present = json.loads(students_present)
absent = json.loads(students_absent)
diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.js b/erpnext/education/doctype/assessment_plan/assessment_plan.js
index c4c56143c3..726c0fcecd 100644
--- a/erpnext/education/doctype/assessment_plan/assessment_plan.js
+++ b/erpnext/education/doctype/assessment_plan/assessment_plan.js
@@ -30,6 +30,23 @@ frappe.ui.form.on('Assessment Plan', {
frappe.set_route('Form', 'Assessment Result Tool');
}, __('Tools'));
}
+
+ frm.set_query('course', function() {
+ return {
+ query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
+ filters: {
+ 'program': frm.doc.program
+ }
+ };
+ });
+
+ frm.set_query('academic_term', function() {
+ return {
+ filters: {
+ 'academic_year': frm.doc.academic_year
+ }
+ };
+ });
},
course: function(frm) {
diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.json b/erpnext/education/doctype/assessment_plan/assessment_plan.json
index 95ed853c21..5066fdf391 100644
--- a/erpnext/education/doctype/assessment_plan/assessment_plan.json
+++ b/erpnext/education/doctype/assessment_plan/assessment_plan.json
@@ -12,8 +12,8 @@
"assessment_group",
"grading_scale",
"column_break_2",
- "course",
"program",
+ "course",
"academic_year",
"academic_term",
"section_break_5",
@@ -198,7 +198,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-05-09 14:56:26.746988",
+ "modified": "2020-10-23 15:55:35.076251",
"modified_by": "Administrator",
"module": "Education",
"name": "Assessment Plan",
diff --git a/erpnext/education/doctype/assessment_result/assessment_result.js b/erpnext/education/doctype/assessment_result/assessment_result.js
index 63d1aee0cb..617a873b82 100644
--- a/erpnext/education/doctype/assessment_result/assessment_result.js
+++ b/erpnext/education/doctype/assessment_result/assessment_result.js
@@ -7,6 +7,23 @@ frappe.ui.form.on('Assessment Result', {
frm.trigger('setup_chart');
}
frm.set_df_property('details', 'read_only', 1);
+
+ frm.set_query('course', function() {
+ return {
+ query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
+ filters: {
+ 'program': frm.doc.program
+ }
+ };
+ });
+
+ frm.set_query('academic_term', function() {
+ return {
+ filters: {
+ 'academic_year': frm.doc.academic_year
+ }
+ };
+ });
},
onload: function(frm) {
diff --git a/erpnext/education/doctype/instructor/instructor.js b/erpnext/education/doctype/instructor/instructor.js
index abb47eda06..24e80fa937 100644
--- a/erpnext/education/doctype/instructor/instructor.js
+++ b/erpnext/education/doctype/instructor/instructor.js
@@ -41,5 +41,24 @@ frappe.ui.form.on("Instructor", {
}
};
});
+
+ frm.set_query("academic_term", "instructor_log", function(_doc, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ return {
+ filters: {
+ "academic_year": d.academic_year
+ }
+ };
+ });
+
+ frm.set_query("course", "instructor_log", function(_doc, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ return {
+ query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses",
+ filters: {
+ "program": d.program
+ }
+ };
+ });
}
});
\ No newline at end of file
diff --git a/erpnext/education/doctype/instructor_log/instructor_log.json b/erpnext/education/doctype/instructor_log/instructor_log.json
index dc9380f2c1..5b9e1f9b97 100644
--- a/erpnext/education/doctype/instructor_log/instructor_log.json
+++ b/erpnext/education/doctype/instructor_log/instructor_log.json
@@ -1,336 +1,88 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-27 08:55:52.680284",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-12-27 08:55:52.680284",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "academic_year",
+ "academic_term",
+ "department",
+ "column_break_3",
+ "program",
+ "course",
+ "student_group",
+ "section_break_8",
+ "other_details"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_year",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Academic Year",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Year",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "academic_year",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Academic Year",
+ "options": "Academic Year",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_term",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Academic Term",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "academic_term",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Academic Term",
+ "options": "Academic Term"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "department",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "program",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Program",
- "length": 0,
- "no_copy": 0,
- "options": "Program",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "program",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Program",
+ "options": "Program",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "course",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Course",
- "length": 0,
- "no_copy": 0,
- "options": "Course",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "course",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Course",
+ "options": "Course"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "student_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Student Group",
- "length": 0,
- "no_copy": 0,
- "options": "Student Group",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "student_group",
+ "fieldtype": "Link",
+ "label": "Student Group",
+ "options": "Student Group"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_8",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "other_details",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Other details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "other_details",
+ "fieldtype": "Small Text",
+ "label": "Other details"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-11-04 03:38:30.902942",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Instructor Log",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-10-23 15:15:50.759657",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Instructor Log",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "restrict_to_domain": "Education",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py
index c1b6850c56..595dff9b9d 100644
--- a/erpnext/education/doctype/student_attendance/student_attendance.py
+++ b/erpnext/education/doctype/student_attendance/student_attendance.py
@@ -6,13 +6,13 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
-from frappe.utils import get_link_to_form
+from frappe.utils import get_link_to_form, getdate
from erpnext.education.api import get_student_group_students
-
class StudentAttendance(Document):
def validate(self):
self.validate_mandatory()
+ self.validate_date()
self.set_date()
self.set_student_group()
self.validate_student()
@@ -27,6 +27,18 @@ class StudentAttendance(Document):
frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'),
frappe.bold('Course Schedule')), title=_('Mandatory Fields'))
+ def validate_date(self):
+ if not self.leave_application and getdate(self.date) > getdate():
+ frappe.throw(_('Attendance cannot be marked for future dates.'))
+
+ if self.student_group:
+ academic_year = frappe.db.get_value('Student Group', self.student_group, 'academic_year')
+ if academic_year:
+ year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date'])
+ if year_start_date and year_end_date:
+ if getdate(self.date) < getdate(year_start_date) or getdate(self.date) > getdate(year_end_date):
+ frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year))
+
def set_student_group(self):
if self.course_schedule:
self.student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group')
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
index 0384505ec2..b59d848828 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js
@@ -52,6 +52,8 @@ frappe.ui.form.on('Student Attendance Tool', {
},
date: function(frm) {
+ if (frm.doc.date > frappe.datetime.get_today())
+ frappe.throw(__("Cannot mark attendance for future dates."));
frm.trigger("student_group");
},
@@ -133,8 +135,8 @@ education.StudentsEditor = Class.extend({
return !stud.disabled && !stud.checked;
});
- frappe.confirm(__("Do you want to update attendance?
Present: {0}\
-
Absent: {1}", [students_present.length, students_absent.length]),
+ frappe.confirm(__("Do you want to update attendance?
Present: {0}
Absent: {1}",
+ [students_present.length, students_absent.length]),
function() { //ifyes
if(!frappe.request.ajax_count) {
frappe.call({
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.json b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.json
index 26b28b3ebe..ee8f4842a3 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.json
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.json
@@ -1,333 +1,118 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-11-16 17:12:46.437539",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2016-11-16 17:12:46.437539",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "based_on",
+ "group_based_on",
+ "column_break_2",
+ "student_group",
+ "academic_year",
+ "academic_term",
+ "course_schedule",
+ "date",
+ "attendance",
+ "students_html"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Student Group\nCourse Schedule",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "based_on",
+ "fieldtype": "Select",
+ "label": "Based On",
+ "options": "Student Group\nCourse Schedule"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Batch",
- "depends_on": "eval:doc.based_on == \"Student Group\"",
- "fieldname": "group_based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Group Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Batch\nCourse\nActivity",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "Batch",
+ "depends_on": "eval:doc.based_on == \"Student Group\"",
+ "fieldname": "group_based_on",
+ "fieldtype": "Select",
+ "label": "Group Based On",
+ "options": "Batch\nCourse\nActivity"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.based_on ==\"Student Group\"",
- "fieldname": "student_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student Group",
- "length": 0,
- "no_copy": 0,
- "options": "Student Group",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.based_on ==\"Student Group\"",
+ "fieldname": "student_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student Group",
+ "options": "Student Group",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.based_on ==\"Course Schedule\"",
- "fieldname": "course_schedule",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Course Schedule",
- "length": 0,
- "no_copy": 0,
- "options": "Course Schedule",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.based_on ==\"Course Schedule\"",
+ "fieldname": "course_schedule",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Course Schedule",
+ "options": "Course Schedule",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.based_on ==\"Student Group\"",
- "fieldname": "date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.based_on ==\"Student Group\"",
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval: (doc.course_schedule \n|| (doc.student_group && doc.date))",
- "fieldname": "attendance",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Attendance",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "depends_on": "eval: (doc.course_schedule \n|| (doc.student_group && doc.date))",
+ "fieldname": "attendance",
+ "fieldtype": "Section Break",
+ "label": "Attendance"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "students_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Students HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "students_html",
+ "fieldtype": "HTML",
+ "label": "Students HTML"
+ },
+ {
+ "fetch_from": "student_group.academic_year",
+ "fieldname": "academic_year",
+ "fieldtype": "Link",
+ "label": "Academic Year",
+ "options": "Academic Year",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "student_group.academic_term",
+ "fieldname": "academic_term",
+ "fieldtype": "Link",
+ "label": "Academic Term",
+ "options": "Academic Term",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 1,
- "hide_toolbar": 1,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-11-10 18:55:36.168044",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Student Attendance Tool",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "hide_toolbar": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-10-23 17:52:28.078971",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Student Attendance Tool",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Instructor",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "Instructor",
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "Academics User",
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "restrict_to_domain": "Education",
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json
index 5e4d59cacf..d91e6bf9dc 100644
--- a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json
+++ b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json
@@ -43,7 +43,8 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Dosage",
- "options": "Prescription Dosage"
+ "options": "Prescription Dosage",
+ "reqd": 1
},
{
"fieldname": "period",
@@ -51,14 +52,16 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Period",
- "options": "Prescription Duration"
+ "options": "Prescription Duration",
+ "reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Dosage Form",
- "options": "Dosage Form"
+ "options": "Dosage Form",
+ "reqd": 1
},
{
"fieldname": "column_break_7",
@@ -72,7 +75,7 @@
"label": "Comment"
},
{
- "depends_on": "use_interval",
+ "depends_on": "usage_interval",
"fieldname": "interval",
"fieldtype": "Int",
"in_list_view": 1,
@@ -80,6 +83,7 @@
},
{
"default": "1",
+ "depends_on": "usage_interval",
"fieldname": "update_schedule",
"fieldtype": "Check",
"hidden": 1,
@@ -99,12 +103,13 @@
"default": "0",
"fieldname": "usage_interval",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Dosage by Time Interval"
}
],
"istable": 1,
"links": [],
- "modified": "2020-02-26 17:02:42.741338",
+ "modified": "2020-09-30 23:32:09.495288",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Drug Prescription",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_entry/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
new file mode 100644
index 0000000000..b953b8adff
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Inpatient Medication Entry', {
+ refresh: function(frm) {
+ // Ignore cancellation of doctype on cancel all
+ frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
+
+ frm.set_query('item_code', () => {
+ return {
+ filters: {
+ is_stock_item: 1
+ }
+ };
+ });
+
+ frm.set_query('drug_code', 'medication_orders', () => {
+ return {
+ filters: {
+ is_stock_item: 1
+ }
+ };
+ });
+ },
+
+ get_medication_orders: function(frm) {
+ frappe.call({
+ method: 'get_medication_orders',
+ doc: frm.doc,
+ freeze: true,
+ freeze_message: __('Fetching Pending Medication Orders'),
+ callback: function() {
+ refresh_field('medication_orders');
+ }
+ });
+ }
+});
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
new file mode 100644
index 0000000000..5d80251b71
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
@@ -0,0 +1,203 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2020-09-25 14:13:20.111906",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "company",
+ "column_break_3",
+ "posting_date",
+ "status",
+ "filters_section",
+ "item_code",
+ "assigned_to_practitioner",
+ "patient",
+ "practitioner",
+ "service_unit",
+ "column_break_11",
+ "from_date",
+ "to_date",
+ "from_time",
+ "to_time",
+ "select_medication_orders_section",
+ "get_medication_orders",
+ "medication_orders",
+ "section_break_18",
+ "update_stock",
+ "warehouse",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "HLC-IME-.YYYY.-"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "filters_section",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code (Drug)",
+ "options": "Item"
+ },
+ {
+ "depends_on": "update_stock",
+ "description": "Warehouse from where medication stock should be consumed",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Medication Warehouse",
+ "mandatory_depends_on": "update_stock",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "label": "Patient",
+ "options": "Patient"
+ },
+ {
+ "fieldname": "service_unit",
+ "fieldtype": "Link",
+ "label": "Healthcare Service Unit",
+ "options": "Healthcare Service Unit"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Date"
+ },
+ {
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Inpatient Medication Entry",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "practitioner",
+ "fieldtype": "Link",
+ "label": "Healthcare Practitioner",
+ "options": "Healthcare Practitioner"
+ },
+ {
+ "fieldname": "select_medication_orders_section",
+ "fieldtype": "Section Break",
+ "label": "Medication Orders"
+ },
+ {
+ "fieldname": "medication_orders",
+ "fieldtype": "Table",
+ "label": "Inpatient Medication Orders",
+ "options": "Inpatient Medication Entry Detail",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.docstatus!==1",
+ "fieldname": "get_medication_orders",
+ "fieldtype": "Button",
+ "label": "Get Pending Medication Orders",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "assigned_to_practitioner",
+ "fieldtype": "Link",
+ "label": "Assigned To",
+ "options": "User"
+ },
+ {
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "label": "Stock Details"
+ },
+ {
+ "default": "1",
+ "fieldname": "update_stock",
+ "fieldtype": "Check",
+ "label": "Update Stock"
+ },
+ {
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "label": "From Time"
+ },
+ {
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "label": "To Time"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-09-30 23:40:45.528715",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Inpatient Medication Entry",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
new file mode 100644
index 0000000000..2385893109
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
@@ -0,0 +1,273 @@
+# -*- 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 _
+from frappe.model.document import Document
+from frappe.utils import flt, get_link_to_form, getdate, nowtime
+from erpnext.stock.utils import get_latest_stock_qty
+from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
+
+class InpatientMedicationEntry(Document):
+ def validate(self):
+ self.validate_medication_orders()
+
+ def get_medication_orders(self):
+ self.validate_datetime_filters()
+
+ # pull inpatient medication orders based on selected filters
+ orders = get_pending_medication_orders(self)
+
+ if orders:
+ self.add_mo_to_table(orders)
+ return self
+ else:
+ self.set('medication_orders', [])
+ 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):
+ # Add medication orders in the child table
+ self.set('medication_orders', [])
+
+ for data in orders:
+ self.append('medication_orders', {
+ 'patient': data.patient,
+ 'patient_name': data.patient_name,
+ 'inpatient_record': data.inpatient_record,
+ 'service_unit': data.service_unit,
+ 'datetime': "%s %s" % (data.date, data.time or "00:00:00"),
+ 'drug_code': data.drug,
+ 'drug_name': data.drug_name,
+ 'dosage': data.dosage,
+ 'dosage_form': data.dosage_form,
+ 'against_imo': data.parent,
+ 'against_imoe': data.name
+ })
+
+ def on_submit(self):
+ self.validate_medication_orders()
+ success_msg = ""
+ if self.update_stock:
+ stock_entry = self.process_stock()
+ success_msg += _('Stock Entry {0} created and ').format(
+ frappe.bold(get_link_to_form('Stock Entry', stock_entry)))
+
+ self.update_medication_orders()
+ success_msg += _('Inpatient Medication Orders updated successfully')
+ frappe.msgprint(success_msg, title=_('Success'), indicator='green')
+
+ def validate_medication_orders(self):
+ for entry in self.medication_orders:
+ docstatus, is_completed = frappe.db.get_value('Inpatient Medication Order Entry', entry.against_imoe,
+ ['docstatus', 'is_completed'])
+
+ if docstatus == 2:
+ frappe.throw(_('Row {0}: Cannot create Inpatient Medication Entry against cancelled Inpatient Medication Order {1}').format(
+ entry.idx, get_link_to_form(entry.against_imo)))
+
+ if is_completed:
+ frappe.throw(_('Row {0}: This Medication Order is already marked as completed').format(
+ entry.idx))
+
+ def on_cancel(self):
+ self.cancel_stock_entries()
+ self.update_medication_orders(on_cancel=True)
+
+ def process_stock(self):
+ allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
+ if not allow_negative_stock:
+ self.check_stock_qty()
+
+ return self.make_stock_entry()
+
+ def update_medication_orders(self, on_cancel=False):
+ orders, order_entry_map = self.get_order_entry_map()
+ # mark completion status
+ is_completed = 1
+ if on_cancel:
+ is_completed = 0
+
+ frappe.db.sql("""
+ UPDATE `tabInpatient Medication Order Entry`
+ SET is_completed = %(is_completed)s
+ WHERE name IN %(orders)s
+ """, {'orders': orders, 'is_completed': is_completed})
+
+ # update status and completed orders count
+ for order, count in order_entry_map.items():
+ medication_order = frappe.get_doc('Inpatient Medication Order', order)
+ completed_orders = flt(count)
+ current_value = frappe.db.get_value('Inpatient Medication Order', order, 'completed_orders')
+
+ if on_cancel:
+ completed_orders = flt(current_value) - flt(count)
+ else:
+ completed_orders = flt(current_value) + flt(count)
+
+ medication_order.db_set('completed_orders', completed_orders)
+ medication_order.set_status()
+
+ def get_order_entry_map(self):
+ # for marking order completion status
+ orders = []
+ # orders mapped
+ order_entry_map = dict()
+
+ for entry in self.medication_orders:
+ orders.append(entry.against_imoe)
+ parent = entry.against_imo
+ if not order_entry_map.get(parent):
+ order_entry_map[parent] = 0
+
+ order_entry_map[parent] += 1
+
+ return orders, order_entry_map
+
+ def check_stock_qty(self):
+ from erpnext.stock.stock_ledger import NegativeStockError
+
+ drug_availability = dict()
+ for d in self.medication_orders:
+ if not drug_availability.get(d.drug_code):
+ drug_availability[d.drug_code] = 0
+ drug_availability[d.drug_code] += flt(d.dosage)
+
+ for drug, dosage in drug_availability.items():
+ available_qty = get_latest_stock_qty(drug, self.warehouse)
+
+ # validate qty
+ if flt(available_qty) < flt(dosage):
+ frappe.throw(_('Quantity not available for {0} in warehouse {1}').format(
+ frappe.bold(drug), frappe.bold(self.warehouse))
+ + '
' + _('Available quantity is {0}, you need {1}').format(
+ frappe.bold(available_qty), frappe.bold(dosage))
+ + '
' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'),
+ NegativeStockError, title=_('Insufficient Stock'))
+
+ def make_stock_entry(self):
+ stock_entry = frappe.new_doc('Stock Entry')
+ stock_entry.purpose = 'Material Issue'
+ stock_entry.set_stock_entry_type()
+ stock_entry.from_warehouse = self.warehouse
+ stock_entry.company = self.company
+ stock_entry.inpatient_medication_entry = self.name
+ cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
+ expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
+
+ for entry in self.medication_orders:
+ se_child = stock_entry.append('items')
+ se_child.item_code = entry.drug_code
+ se_child.item_name = entry.drug_name
+ se_child.uom = frappe.db.get_value('Item', entry.drug_code, 'stock_uom')
+ se_child.stock_uom = se_child.uom
+ se_child.qty = flt(entry.dosage)
+ # in stock uom
+ se_child.conversion_factor = 1
+ se_child.cost_center = cost_center
+ se_child.expense_account = expense_account
+ # references
+ se_child.patient = entry.patient
+ se_child.inpatient_medication_entry_child = entry.name
+
+ stock_entry.submit()
+ return stock_entry.name
+
+ def cancel_stock_entries(self):
+ stock_entries = frappe.get_all('Stock Entry', {'inpatient_medication_entry': self.name})
+ for entry in stock_entries:
+ doc = frappe.get_doc('Stock Entry', entry.name)
+ doc.cancel()
+
+
+def get_pending_medication_orders(entry):
+ filters, values = get_filters(entry)
+
+ data = frappe.db.sql("""
+ SELECT
+ ip.inpatient_record, ip.patient, ip.patient_name,
+ entry.name, entry.parent, entry.drug, entry.drug_name,
+ entry.dosage, entry.dosage_form, entry.date, entry.time, entry.instructions
+ FROM
+ `tabInpatient Medication Order` ip
+ INNER JOIN
+ `tabInpatient Medication Order Entry` entry
+ ON
+ ip.name = entry.parent
+ WHERE
+ ip.docstatus = 1 and
+ ip.company = %(company)s and
+ entry.is_completed = 0
+ {0}
+ ORDER BY
+ entry.date, entry.time
+ """.format(filters), values, as_dict=1)
+
+ for doc in data:
+ inpatient_record = doc.inpatient_record
+ doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
+
+ if entry.service_unit and doc.service_unit != entry.service_unit:
+ data.remove(doc)
+
+ return data
+
+
+def get_filters(entry):
+ filters = ''
+ values = dict(company=entry.company)
+ if entry.from_date:
+ filters += ' and entry.date >= %(from_date)s'
+ values['from_date'] = entry.from_date
+
+ if entry.to_date:
+ filters += ' and entry.date <= %(to_date)s'
+ values['to_date'] = entry.to_date
+
+ if entry.from_time:
+ filters += ' and entry.time >= %(from_time)s'
+ values['from_time'] = entry.from_time
+
+ if entry.to_time:
+ filters += ' and entry.time <= %(to_time)s'
+ values['to_time'] = entry.to_time
+
+ if entry.patient:
+ filters += ' and ip.patient = %(patient)s'
+ values['patient'] = entry.patient
+
+ if entry.practitioner:
+ filters += ' and ip.practitioner = %(practitioner)s'
+ values['practitioner'] = entry.practitioner
+
+ if entry.item_code:
+ filters += ' and entry.drug = %(item_code)s'
+ values['item_code'] = entry.item_code
+
+ if entry.assigned_to_practitioner:
+ filters += ' and ip._assign LIKE %(assigned_to)s'
+ values['assigned_to'] = '%' + entry.assigned_to_practitioner + '%'
+
+ return filters, values
+
+
+def get_current_healthcare_service_unit(inpatient_record):
+ ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
+ return ip_record.inpatient_occupancies[-1].service_unit
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py
new file mode 100644
index 0000000000..a4bec45596
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'against_imoe',
+ 'internal_links': {
+ 'Inpatient Medication Order': ['medication_orders', 'against_imo']
+ },
+ 'transactions': [
+ {
+ 'label': _('Reference'),
+ 'items': ['Inpatient Medication Order']
+ }
+ ]
+ }
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py
new file mode 100644
index 0000000000..2f1bb6b56f
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import add_days, getdate, now_datetime
+from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
+from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
+from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
+
+class TestInpatientMedicationEntry(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ frappe.db.sql("""delete from `tabInpatient Medication Order`""")
+ frappe.db.sql("""delete from `tabInpatient Medication Entry`""")
+ self.patient = create_patient()
+
+ # Admit
+ ip_record = create_inpatient(self.patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save()
+ ip_record.reload()
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+ self.ip_record = ip_record
+
+ def test_filters_for_fetching_pending_mo(self):
+ ipmo = create_ipmo(self.patient)
+ ipmo.submit()
+ ipmo.reload()
+
+ date = add_days(getdate(), -1)
+ filters = frappe._dict(
+ from_date=date,
+ to_date=date,
+ from_time='',
+ to_time='',
+ item_code='Dextromethorphan',
+ patient=self.patient
+ )
+
+ ipme = create_ipme(filters, update_stock=0)
+
+ # 3 dosages per day
+ self.assertEqual(len(ipme.medication_orders), 3)
+ self.assertEqual(getdate(ipme.medication_orders[0].datetime), date)
+
+ def test_ipme_with_stock_update(self):
+ ipmo = create_ipmo(self.patient)
+ ipmo.submit()
+ ipmo.reload()
+
+ date = add_days(getdate(), -1)
+ filters = frappe._dict(
+ from_date=date,
+ to_date=date,
+ from_time='',
+ to_time='',
+ item_code='Dextromethorphan',
+ patient=self.patient
+ )
+
+ make_stock_entry()
+ ipme = create_ipme(filters, update_stock=1)
+ ipme.submit()
+ ipme.reload()
+
+ # test order completed
+ is_order_completed = frappe.db.get_value('Inpatient Medication Order Entry',
+ ipme.medication_orders[0].against_imoe, 'is_completed')
+ self.assertEqual(is_order_completed, 1)
+
+ # test stock entry
+ stock_entry = frappe.db.exists('Stock Entry', {'inpatient_medication_entry': ipme.name})
+ self.assertTrue(stock_entry)
+
+ # check references
+ stock_entry = frappe.get_doc('Stock Entry', stock_entry)
+ self.assertEqual(stock_entry.items[0].patient, self.patient)
+ self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name)
+
+ def tearDown(self):
+ # cleanup - Discharge
+ schedule_discharge(frappe.as_json({'patient': self.patient}))
+ self.ip_record.reload()
+ mark_invoiced_inpatient_occupancy(self.ip_record)
+
+ self.ip_record.reload()
+ discharge_patient(self.ip_record)
+
+ for entry in frappe.get_all('Inpatient Medication Entry'):
+ doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
+ doc.cancel()
+ frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name})
+ doc.delete()
+
+ for entry in frappe.get_all('Inpatient Medication Order'):
+ doc = frappe.get_doc('Inpatient Medication Order', entry.name)
+ doc.cancel()
+ doc.delete()
+
+def make_stock_entry():
+ frappe.db.set_value('Company', '_Test Company', {
+ 'stock_adjustment_account': 'Stock Adjustment - _TC',
+ 'default_inventory_account': 'Stock In Hand - _TC'
+ })
+ stock_entry = frappe.new_doc('Stock Entry')
+ stock_entry.stock_entry_type = 'Material Receipt'
+ stock_entry.company = '_Test Company'
+ stock_entry.to_warehouse = 'Stores - _TC'
+ expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company')
+ se_child = stock_entry.append('items')
+ se_child.item_code = 'Dextromethorphan'
+ se_child.item_name = 'Dextromethorphan'
+ se_child.uom = 'Nos'
+ se_child.stock_uom = 'Nos'
+ se_child.qty = 6
+ se_child.t_warehouse = 'Stores - _TC'
+ # in stock uom
+ se_child.conversion_factor = 1.0
+ se_child.expense_account = expense_account
+ stock_entry.submit()
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry_detail/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json
new file mode 100644
index 0000000000..e3d7212169
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json
@@ -0,0 +1,163 @@
+{
+ "actions": [],
+ "creation": "2020-09-25 14:56:32.636569",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "patient",
+ "patient_name",
+ "inpatient_record",
+ "column_break_4",
+ "service_unit",
+ "datetime",
+ "medication_details_section",
+ "drug_code",
+ "drug_name",
+ "dosage",
+ "available_qty",
+ "dosage_form",
+ "column_break_10",
+ "instructions",
+ "references_section",
+ "against_imo",
+ "against_imoe"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Patient",
+ "options": "Patient",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "patient.patient_name",
+ "fieldname": "patient_name",
+ "fieldtype": "Data",
+ "label": "Patient Name",
+ "read_only": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "drug_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Drug Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "drug_code.item_name",
+ "fieldname": "drug_name",
+ "fieldtype": "Data",
+ "label": "Drug Name",
+ "read_only": 1
+ },
+ {
+ "columns": 1,
+ "fieldname": "dosage",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Dosage",
+ "reqd": 1
+ },
+ {
+ "fieldname": "dosage_form",
+ "fieldtype": "Link",
+ "label": "Dosage Form",
+ "options": "Dosage Form"
+ },
+ {
+ "fetch_from": "patient.inpatient_record",
+ "fieldname": "inpatient_record",
+ "fieldtype": "Link",
+ "label": "Inpatient Record",
+ "options": "Inpatient Record",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "references_section",
+ "fieldtype": "Section Break",
+ "label": "References"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "medication_details_section",
+ "fieldtype": "Section Break",
+ "label": "Medication Details"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 3,
+ "fieldname": "datetime",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Datetime",
+ "reqd": 1
+ },
+ {
+ "fieldname": "instructions",
+ "fieldtype": "Small Text",
+ "label": "Instructions"
+ },
+ {
+ "columns": 2,
+ "fieldname": "service_unit",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Service Unit",
+ "options": "Healthcare Service Unit",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "against_imo",
+ "fieldtype": "Link",
+ "label": "Against Inpatient Medication Order",
+ "no_copy": 1,
+ "options": "Inpatient Medication Order",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "against_imoe",
+ "fieldtype": "Data",
+ "label": "Against Inpatient Medication Order Entry",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "available_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Available Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-09-30 14:48:23.648223",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Inpatient Medication Entry Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py
new file mode 100644
index 0000000000..644898d9ed
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class InpatientMedicationEntryDetail(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_order/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js
new file mode 100644
index 0000000000..c51f3cf882
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js
@@ -0,0 +1,106 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Inpatient Medication Order', {
+ refresh: function(frm) {
+ if (frm.doc.docstatus === 1) {
+ frm.trigger("show_progress");
+ }
+
+ frm.events.show_medication_order_button(frm);
+
+ frm.set_query('patient', () => {
+ return {
+ filters: {
+ 'inpatient_record': ['!=', '']
+ }
+ };
+ });
+ },
+
+ show_medication_order_button: function(frm) {
+ frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
+ frm.fields_dict['medication_orders'].grid.add_custom_button(__('Add Medication Orders'), () => {
+ let d = new frappe.ui.Dialog({
+ title: __('Add Medication Orders'),
+ fields: [
+ {
+ fieldname: 'drug_code',
+ label: __('Drug'),
+ fieldtype: 'Link',
+ options: 'Item',
+ reqd: 1,
+ "get_query": function () {
+ return {
+ filters: {'is_stock_item': 1}
+ };
+ }
+ },
+ {
+ fieldname: 'dosage',
+ label: __('Dosage'),
+ fieldtype: 'Link',
+ options: 'Prescription Dosage',
+ reqd: 1
+ },
+ {
+ fieldname: 'period',
+ label: __('Period'),
+ fieldtype: 'Link',
+ options: 'Prescription Duration',
+ reqd: 1
+ },
+ {
+ fieldname: 'dosage_form',
+ label: __('Dosage Form'),
+ fieldtype: 'Link',
+ options: 'Dosage Form',
+ reqd: 1
+ }
+ ],
+ primary_action_label: __('Add'),
+ primary_action: () => {
+ let values = d.get_values();
+ if (values) {
+ frm.call({
+ doc: frm.doc,
+ method: 'add_order_entries',
+ args: {
+ order: values
+ },
+ freeze: true,
+ freeze_message: __('Adding Order Entries'),
+ callback: function() {
+ frm.refresh_field('medication_orders');
+ }
+ });
+ }
+ },
+ });
+ d.show();
+ });
+ },
+
+ show_progress: function(frm) {
+ let bars = [];
+ let message = '';
+
+ // completed sessions
+ let title = __('{0} medication orders completed', [frm.doc.completed_orders]);
+ if (frm.doc.completed_orders === 1) {
+ title = __('{0} medication order completed', [frm.doc.completed_orders]);
+ }
+ title += __(' out of {0}', [frm.doc.total_orders]);
+
+ bars.push({
+ 'title': title,
+ 'width': (frm.doc.completed_orders / frm.doc.total_orders * 100) + '%',
+ 'progress_class': 'progress-bar-success'
+ });
+ if (bars[0].width == '0%') {
+ bars[0].width = '0.5%';
+ }
+ message = title;
+ frm.dashboard.add_progress(__('Status'), bars, message);
+ }
+});
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json
new file mode 100644
index 0000000000..e31d2e3e36
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json
@@ -0,0 +1,196 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2020-09-14 18:33:56.715736",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "patient_details_section",
+ "naming_series",
+ "patient_encounter",
+ "patient",
+ "patient_name",
+ "patient_age",
+ "inpatient_record",
+ "column_break_6",
+ "company",
+ "status",
+ "practitioner",
+ "start_date",
+ "end_date",
+ "medication_orders_section",
+ "medication_orders",
+ "section_break_16",
+ "total_orders",
+ "column_break_18",
+ "completed_orders",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "patient_details_section",
+ "fieldtype": "Section Break",
+ "label": "Patient Details"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "HLC-IMO-.YYYY.-"
+ },
+ {
+ "fieldname": "patient_encounter",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Patient Encounter",
+ "options": "Patient Encounter"
+ },
+ {
+ "fetch_from": "patient_encounter.patient",
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "label": "Patient",
+ "options": "Patient",
+ "read_only_depends_on": "patient_encounter",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "patient.patient_name",
+ "fieldname": "patient_name",
+ "fieldtype": "Data",
+ "label": "Patient Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "patient_age",
+ "fieldtype": "Data",
+ "label": "Patient Age",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "patient.inpatient_record",
+ "fieldname": "inpatient_record",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Inpatient Record",
+ "options": "Inpatient Record",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fetch_from": "patient_encounter.practitioner",
+ "fieldname": "practitioner",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Healthcare Practitioner",
+ "options": "Healthcare Practitioner",
+ "read_only_depends_on": "patient_encounter"
+ },
+ {
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Start Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "label": "End Date",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.patient && doc.start_date",
+ "fieldname": "medication_orders_section",
+ "fieldtype": "Section Break",
+ "label": "Medication Orders"
+ },
+ {
+ "fieldname": "medication_orders",
+ "fieldtype": "Table",
+ "label": "Medication Orders",
+ "options": "Inpatient Medication Order Entry"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Inpatient Medication Order",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break",
+ "label": "Other Details"
+ },
+ {
+ "fieldname": "total_orders",
+ "fieldtype": "Float",
+ "label": "Total Orders",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "completed_orders",
+ "fieldtype": "Float",
+ "label": "Completed Orders",
+ "no_copy": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-09-30 21:53:27.128591",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Inpatient Medication Order",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "patient_encounter, patient",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "patient",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
new file mode 100644
index 0000000000..33cbbec812
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
@@ -0,0 +1,74 @@
+# -*- 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 _
+from frappe.model.document import Document
+from frappe.utils import cstr
+from erpnext.healthcare.doctype.patient_encounter.patient_encounter import get_prescription_dates
+
+class InpatientMedicationOrder(Document):
+ def validate(self):
+ self.validate_inpatient()
+ self.validate_duplicate()
+ self.set_total_orders()
+ self.set_status()
+
+ def on_submit(self):
+ self.validate_inpatient()
+ self.set_status()
+
+ def on_cancel(self):
+ self.set_status()
+
+ def validate_inpatient(self):
+ if not self.inpatient_record:
+ frappe.throw(_('No Inpatient Record found against patient {0}').format(self.patient))
+
+ def validate_duplicate(self):
+ existing_mo = frappe.db.exists('Inpatient Medication Order', {
+ 'patient_encounter': self.patient_encounter,
+ 'docstatus': ('!=', 2),
+ 'name': ('!=', self.name)
+ })
+ if existing_mo:
+ frappe.throw(_('An Inpatient Medication Order {0} against Patient Encounter {1} already exists.').format(
+ existing_mo, self.patient_encounter), frappe.DuplicateEntryError)
+
+ def set_total_orders(self):
+ self.db_set('total_orders', len(self.medication_orders))
+
+ def set_status(self):
+ status = {
+ "0": "Draft",
+ "1": "Submitted",
+ "2": "Cancelled"
+ }[cstr(self.docstatus or 0)]
+
+ if self.docstatus == 1:
+ if not self.completed_orders:
+ status = 'Pending'
+ elif self.completed_orders < self.total_orders:
+ status = 'In Process'
+ else:
+ status = 'Completed'
+
+ self.db_set('status', status)
+
+ def add_order_entries(self, order):
+ if order.get('drug_code'):
+ dosage = frappe.get_doc('Prescription Dosage', order.get('dosage'))
+ dates = get_prescription_dates(order.get('period'), self.start_date)
+ for date in dates:
+ for dose in dosage.dosage_strength:
+ entry = self.append('medication_orders')
+ entry.drug = order.get('drug_code')
+ entry.drug_name = frappe.db.get_value('Item', order.get('drug_code'), 'item_name')
+ entry.dosage = dose.strength
+ entry.dosage_form = order.get('dosage_form')
+ entry.date = date
+ entry.time = dose.strength_time
+ self.end_date = dates[-1]
+ return
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js
new file mode 100644
index 0000000000..1c318768ea
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js
@@ -0,0 +1,16 @@
+frappe.listview_settings['Inpatient Medication Order'] = {
+ add_fields: ["status"],
+ filters: [["status", "!=", "Cancelled"]],
+ get_indicator: function(doc) {
+ if (doc.status === "Pending") {
+ return [__("Pending"), "orange", "status,=,Pending"];
+
+ } else if (doc.status === "In Process") {
+ return [__("In Process"), "blue", "status,=,In Process"];
+
+ } else if (doc.status === "Completed") {
+ return [__("Completed"), "green", "status,=,Completed"];
+
+ }
+ }
+};
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
new file mode 100644
index 0000000000..a21caca8ff
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import add_days, getdate, now_datetime
+from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
+from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+
+class TestInpatientMedicationOrder(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ self.patient = create_patient()
+
+ # Admit
+ ip_record = create_inpatient(self.patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save()
+ ip_record.reload()
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+ self.ip_record = ip_record
+
+ def test_order_creation(self):
+ ipmo = create_ipmo(self.patient)
+ ipmo.submit()
+ ipmo.reload()
+
+ # 3 dosages per day for 2 days
+ self.assertEqual(len(ipmo.medication_orders), 6)
+ self.assertEqual(ipmo.medication_orders[0].date, add_days(getdate(), -1))
+
+ prescription_dosage = frappe.get_doc('Prescription Dosage', '1-1-1')
+ for i in range(len(prescription_dosage.dosage_strength)):
+ self.assertEqual(ipmo.medication_orders[i].time, prescription_dosage.dosage_strength[i].strength_time)
+
+ self.assertEqual(ipmo.medication_orders[3].date, getdate())
+
+ def test_inpatient_validation(self):
+ # Discharge
+ schedule_discharge(frappe.as_json({'patient': self.patient}))
+
+ self.ip_record.reload()
+ mark_invoiced_inpatient_occupancy(self.ip_record)
+
+ self.ip_record.reload()
+ discharge_patient(self.ip_record)
+
+ ipmo = create_ipmo(self.patient)
+ # inpatient validation
+ self.assertRaises(frappe.ValidationError, ipmo.insert)
+
+ def test_status(self):
+ ipmo = create_ipmo(self.patient)
+ ipmo.submit()
+ ipmo.reload()
+
+ self.assertEqual(ipmo.status, 'Pending')
+
+ filters = frappe._dict(from_date=add_days(getdate(), -1), to_date=add_days(getdate(), -1), from_time='', to_time='')
+ ipme = create_ipme(filters)
+ ipme.submit()
+ ipmo.reload()
+ self.assertEqual(ipmo.status, 'In Process')
+
+ filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='')
+ ipme = create_ipme(filters)
+ ipme.submit()
+ ipmo.reload()
+ self.assertEqual(ipmo.status, 'Completed')
+
+ def tearDown(self):
+ if frappe.db.get_value('Patient', self.patient, 'inpatient_record'):
+ # cleanup - Discharge
+ schedule_discharge(frappe.as_json({'patient': self.patient}))
+ self.ip_record.reload()
+ mark_invoiced_inpatient_occupancy(self.ip_record)
+
+ self.ip_record.reload()
+ discharge_patient(self.ip_record)
+
+ for entry in frappe.get_all('Inpatient Medication Entry'):
+ doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
+ doc.cancel()
+ doc.delete()
+
+ for entry in frappe.get_all('Inpatient Medication Order'):
+ doc = frappe.get_doc('Inpatient Medication Order', entry.name)
+ doc.cancel()
+ doc.delete()
+
+def create_dosage_form():
+ if not frappe.db.exists('Dosage Form', 'Tablet'):
+ frappe.get_doc({
+ 'doctype': 'Dosage Form',
+ 'dosage_form': 'Tablet'
+ }).insert()
+
+def create_drug(item=None):
+ if not item:
+ item = 'Dextromethorphan'
+ drug = frappe.db.exists('Item', {'item_code': 'Dextromethorphan'})
+ if not drug:
+ drug = frappe.get_doc({
+ 'doctype': 'Item',
+ 'item_code': 'Dextromethorphan',
+ 'item_name': 'Dextromethorphan',
+ 'item_group': 'Products',
+ 'stock_uom': 'Nos',
+ 'is_stock_item': 1,
+ 'valuation_rate': 50,
+ 'opening_stock': 20
+ }).insert()
+
+def get_orders():
+ create_dosage_form()
+ create_drug()
+ return {
+ 'drug_code': 'Dextromethorphan',
+ 'drug_name': 'Dextromethorphan',
+ 'dosage': '1-1-1',
+ 'dosage_form': 'Tablet',
+ 'period': '2 Day'
+ }
+
+def create_ipmo(patient):
+ orders = get_orders()
+ ipmo = frappe.new_doc('Inpatient Medication Order')
+ ipmo.patient = patient
+ ipmo.company = '_Test Company'
+ ipmo.start_date = add_days(getdate(), -1)
+ ipmo.add_order_entries(orders)
+
+ return ipmo
+
+def create_ipme(filters, update_stock=0):
+ ipme = frappe.new_doc('Inpatient Medication Entry')
+ ipme.company = '_Test Company'
+ ipme.posting_date = getdate()
+ ipme.update_stock = update_stock
+ if update_stock:
+ ipme.warehouse = 'Stores - _TC'
+ for key, value in filters.items():
+ ipme.set(key, value)
+ ipme = ipme.get_medication_orders()
+
+ return ipme
+
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order_entry/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_order_entry/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json
new file mode 100644
index 0000000000..72999a908e
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json
@@ -0,0 +1,94 @@
+{
+ "actions": [],
+ "creation": "2020-09-14 21:51:30.259164",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "drug",
+ "drug_name",
+ "dosage",
+ "dosage_form",
+ "instructions",
+ "column_break_4",
+ "date",
+ "time",
+ "is_completed"
+ ],
+ "fields": [
+ {
+ "fieldname": "drug",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Drug",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "drug.item_name",
+ "fieldname": "drug_name",
+ "fieldtype": "Data",
+ "label": "Drug Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "dosage",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Dosage",
+ "reqd": 1
+ },
+ {
+ "fieldname": "dosage_form",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Dosage Form",
+ "options": "Dosage Form",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "Time",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_completed",
+ "fieldtype": "Check",
+ "label": "Is Order Completed",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "instructions",
+ "fieldtype": "Small Text",
+ "label": "Instructions"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-09-30 14:03:26.755925",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Inpatient Medication Order Entry",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py
new file mode 100644
index 0000000000..ebfe366346
--- /dev/null
+++ b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class InpatientMedicationOrderEntry(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
index 2bef5fb5bd..70706adb2e 100644
--- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
@@ -83,6 +83,7 @@ def get_healthcare_service_unit():
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy"
+ service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
service_unit.occupancy_status = "Vacant"
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
index 6353d19ef1..e960f0a9c4 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
@@ -58,6 +58,14 @@ frappe.ui.form.on('Patient Encounter', {
create_procedure(frm);
},'Create');
+ if (frm.doc.drug_prescription && frm.doc.inpatient_record && frm.doc.inpatient_status === "Admitted") {
+ frm.add_custom_button(__('Inpatient Medication Order'), function() {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.healthcare.doctype.patient_encounter.patient_encounter.make_ip_medication_order',
+ frm: frm
+ });
+ }, 'Create');
+ }
}
frm.set_query('patient', function() {
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
index 262fc4650a..87f42491fc 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
@@ -6,8 +6,9 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cstr
+from frappe.utils import cstr, getdate, add_days
from frappe import _
+from frappe.model.mapper import get_mapped_doc
class PatientEncounter(Document):
def validate(self):
@@ -22,20 +23,69 @@ class PatientEncounter(Document):
insert_encounter_to_medical_record(self)
def on_submit(self):
- update_encounter_medical_record(self)
+ if self.therapies:
+ create_therapy_plan(self)
def on_cancel(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
- delete_medical_record(self)
- def on_submit(self):
- create_therapy_plan(self)
+ if self.inpatient_record and self.drug_prescription:
+ delete_ip_medication_order(self)
+
+ delete_medical_record(self)
def set_title(self):
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100]
+@frappe.whitelist()
+def make_ip_medication_order(source_name, target_doc=None):
+ def set_missing_values(source, target):
+ target.start_date = source.encounter_date
+ for entry in source.drug_prescription:
+ if entry.drug_code:
+ dosage = frappe.get_doc('Prescription Dosage', entry.dosage)
+ dates = get_prescription_dates(entry.period, target.start_date)
+ for date in dates:
+ for dose in dosage.dosage_strength:
+ order = target.append('medication_orders')
+ order.drug = entry.drug_code
+ order.drug_name = entry.drug_name
+ order.dosage = dose.strength
+ order.instructions = entry.comment
+ order.dosage_form = entry.dosage_form
+ order.date = date
+ order.time = dose.strength_time
+ target.end_date = dates[-1]
+
+ doc = get_mapped_doc('Patient Encounter', source_name, {
+ 'Patient Encounter': {
+ 'doctype': 'Inpatient Medication Order',
+ 'field_map': {
+ 'name': 'patient_encounter',
+ 'patient': 'patient',
+ 'patient_name': 'patient_name',
+ 'patient_age': 'patient_age',
+ 'inpatient_record': 'inpatient_record',
+ 'practitioner': 'practitioner',
+ 'start_date': 'encounter_date'
+ },
+ }
+ }, target_doc, set_missing_values)
+
+ return doc
+
+
+def get_prescription_dates(period, start_date):
+ prescription_duration = frappe.get_doc('Prescription Duration', period)
+ days = prescription_duration.get_days()
+ dates = [start_date]
+ for i in range(1, days):
+ dates.append(add_days(getdate(start_date), i))
+ return dates
+
+
def create_therapy_plan(encounter):
if len(encounter.therapies):
doc = frappe.new_doc('Therapy Plan')
@@ -51,6 +101,7 @@ def create_therapy_plan(encounter):
encounter.db_set('therapy_plan', doc.name)
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
+
def insert_encounter_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
@@ -63,6 +114,7 @@ def insert_encounter_to_medical_record(doc):
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})
@@ -72,8 +124,17 @@ def update_encounter_medical_record(encounter):
else:
insert_encounter_to_medical_record(encounter)
+
def delete_medical_record(encounter):
- frappe.delete_doc_if_exists('Patient Medical Record', 'reference_name', encounter.name)
+ record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name})
+ if record:
+ frappe.delete_doc('Patient Medical Record', record, force=1)
+
+def delete_ip_medication_order(encounter):
+ record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name})
+ if record:
+ frappe.delete_doc('Inpatient Medication Order', record, force=1)
+
def set_subject_field(encounter):
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '
'
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py
index b08b172bba..39e54f5b35 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py
@@ -5,12 +5,18 @@ def get_data():
return {
'fieldname': 'encounter',
'non_standard_fieldnames': {
- 'Patient Medical Record': 'reference_name'
+ 'Patient Medical Record': 'reference_name',
+ 'Inpatient Medication Order': 'patient_encounter'
},
'transactions': [
{
'label': _('Records'),
'items': ['Vital Signs', 'Patient Medical Record']
},
- ]
+ {
+ 'label': _('Orders'),
+ 'items': ['Inpatient Medication Order']
+ }
+ ],
+ 'disable_create_buttons': ['Inpatient Medication Order']
}
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index 526bb95b70..a061c66a54 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -5,9 +5,9 @@ from __future__ import unicode_literals
import frappe
import unittest
-from frappe.utils import getdate
+from frappe.utils import getdate, flt
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
-from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session
+from erpnext.healthcare.doctype.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
class TestTherapyPlan(unittest.TestCase):
@@ -20,25 +20,45 @@ class TestTherapyPlan(unittest.TestCase):
plan = create_therapy_plan()
self.assertEquals(plan.status, 'Not Started')
- session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab')
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
- session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab')
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
+ def test_therapy_plan_from_template(self):
+ patient = create_patient()
+ template = create_therapy_plan_template()
+ # check linked item
+ self.assertTrue(frappe.db.exists('Therapy Plan Template', {'linked_item': 'Complete Rehab'}))
-def create_therapy_plan():
+ plan = create_therapy_plan(template)
+ # invoice
+ si = make_sales_invoice(plan.name, patient, '_Test Company', template)
+ si.save()
+
+ therapy_plan_template_amt = frappe.db.get_value('Therapy Plan Template', template, 'total_amount')
+ self.assertEquals(si.items[0].amount, therapy_plan_template_amt)
+
+
+def create_therapy_plan(template=None):
patient = create_patient()
therapy_type = create_therapy_type()
plan = frappe.new_doc('Therapy Plan')
plan.patient = patient
plan.start_date = getdate()
- plan.append('therapy_plan_details', {
- 'therapy_type': therapy_type.name,
- 'no_of_sessions': 2
- })
+
+ if template:
+ plan.therapy_plan_template = template
+ plan = plan.set_therapy_details_from_template()
+ else:
+ plan.append('therapy_plan_details', {
+ 'therapy_type': therapy_type.name,
+ 'no_of_sessions': 2
+ })
+
plan.save()
return plan
@@ -55,3 +75,22 @@ def create_encounter(patient, medical_department, practitioner):
encounter.save()
encounter.submit()
return encounter
+
+def create_therapy_plan_template():
+ template_name = frappe.db.exists('Therapy Plan Template', 'Complete Rehab')
+ if not template_name:
+ therapy_type = create_therapy_type()
+ template = frappe.new_doc('Therapy Plan Template')
+ template.plan_name = template.item_code = template.item_name = 'Complete Rehab'
+ template.item_group = 'Services'
+ rate = frappe.db.get_value('Therapy Type', therapy_type.name, 'rate')
+ template.append('therapy_types', {
+ 'therapy_type': therapy_type.name,
+ 'no_of_sessions': 2,
+ 'rate': rate,
+ 'amount': 2 * flt(rate)
+ })
+ template.save()
+ template_name = template.name
+
+ return template_name
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
index dea0cfeb84..490d4588d1 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
@@ -37,7 +37,8 @@ frappe.ui.form.on('Therapy Plan', {
args: {
therapy_plan: frm.doc.name,
patient: frm.doc.patient,
- therapy_type: data.therapy_type
+ therapy_type: data.therapy_type,
+ company: frm.doc.company
},
freeze: true,
callback: function(r) {
@@ -49,13 +50,53 @@ frappe.ui.form.on('Therapy Plan', {
});
}, __('Select Therapy Type'), __('Create'));
}, __('Create'));
+
+ if (frm.doc.therapy_plan_template && !frm.doc.invoiced) {
+ frm.add_custom_button(__('Sales Invoice'), function() {
+ frm.trigger('make_sales_invoice');
+ }, __('Create'));
+ }
+ }
+
+ if (frm.doc.therapy_plan_template) {
+ frappe.meta.get_docfield('Therapy Plan Detail', 'therapy_type', frm.doc.name).read_only = 1;
+ frappe.meta.get_docfield('Therapy Plan Detail', 'no_of_sessions', frm.doc.name).read_only = 1;
+ }
+ },
+
+ make_sales_invoice: function(frm) {
+ frappe.call({
+ args: {
+ 'reference_name': frm.doc.name,
+ 'patient': frm.doc.patient,
+ 'company': frm.doc.company,
+ 'therapy_plan_template': frm.doc.therapy_plan_template
+ },
+ method: 'erpnext.healthcare.doctype.therapy_plan.therapy_plan.make_sales_invoice',
+ callback: function(r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route('Form', doclist[0].doctype, doclist[0].name);
+ }
+ });
+ },
+
+ therapy_plan_template: function(frm) {
+ if (frm.doc.therapy_plan_template) {
+ frappe.call({
+ method: 'set_therapy_details_from_template',
+ doc: frm.doc,
+ freeze: true,
+ freeze_message: __('Fetching Template Details'),
+ callback: function() {
+ refresh_field('therapy_plan_details');
+ }
+ });
}
},
show_progress_for_therapies: function(frm) {
let bars = [];
let message = '';
- let added_min = false;
// completed sessions
let title = __('{0} sessions completed', [frm.doc.total_sessions_completed]);
@@ -71,7 +112,6 @@ frappe.ui.form.on('Therapy Plan', {
});
if (bars[0].width == '0%') {
bars[0].width = '0.5%';
- added_min = 0.5;
}
message = title;
frm.dashboard.add_progress(__('Status'), bars, message);
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json
index 9edfeb2faa..ccb316e5c3 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json
@@ -9,11 +9,13 @@
"naming_series",
"patient",
"patient_name",
+ "invoiced",
"column_break_4",
"company",
"status",
"start_date",
"section_break_3",
+ "therapy_plan_template",
"therapy_plan_details",
"title",
"section_break_9",
@@ -46,6 +48,7 @@
"fieldtype": "Table",
"label": "Therapy Plan Details",
"options": "Therapy Plan Detail",
+ "read_only_depends_on": "therapy_plan_template",
"reqd": 1
},
{
@@ -105,11 +108,27 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
- "options": "Company"
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "therapy_plan_template",
+ "fieldtype": "Link",
+ "label": "Therapy Plan Template",
+ "options": "Therapy Plan Template"
+ },
+ {
+ "default": "0",
+ "fieldname": "invoiced",
+ "fieldtype": "Check",
+ "label": "Invoiced",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"links": [],
- "modified": "2020-05-25 14:38:53.649315",
+ "modified": "2020-10-23 01:27:42.128855",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan",
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index e0f015f3d7..bc0ff1a505 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
-from frappe.utils import today
+from frappe.utils import flt, today
class TherapyPlan(Document):
def validate(self):
@@ -33,13 +33,26 @@ class TherapyPlan(Document):
self.db_set('total_sessions', total_sessions)
self.db_set('total_sessions_completed', total_sessions_completed)
+ def set_therapy_details_from_template(self):
+ # Add therapy types in the child table
+ self.set('therapy_plan_details', [])
+ therapy_plan_template = frappe.get_doc('Therapy Plan Template', self.therapy_plan_template)
+
+ for data in therapy_plan_template.therapy_types:
+ self.append('therapy_plan_details', {
+ 'therapy_type': data.therapy_type,
+ 'no_of_sessions': data.no_of_sessions
+ })
+ return self
+
@frappe.whitelist()
-def make_therapy_session(therapy_plan, patient, therapy_type):
+def make_therapy_session(therapy_plan, patient, therapy_type, company):
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session')
therapy_session.therapy_plan = therapy_plan
+ therapy_session.company = company
therapy_session.patient = patient
therapy_session.therapy_type = therapy_type.name
therapy_session.duration = therapy_type.default_duration
@@ -48,4 +61,39 @@ def make_therapy_session(therapy_plan, patient, therapy_type):
if frappe.flags.in_test:
therapy_session.start_date = today()
- return therapy_session.as_dict()
\ No newline at end of file
+ return therapy_session.as_dict()
+
+
+@frappe.whitelist()
+def make_sales_invoice(reference_name, patient, company, therapy_plan_template):
+ from erpnext.stock.get_item_details import get_item_details
+ si = frappe.new_doc('Sales Invoice')
+ si.company = company
+ si.patient = patient
+ si.customer = frappe.db.get_value('Patient', patient, 'customer')
+
+ item = frappe.db.get_value('Therapy Plan Template', therapy_plan_template, 'linked_item')
+ price_list, price_list_currency = frappe.db.get_values('Price List', {'selling': 1}, ['name', 'currency'])[0]
+ args = {
+ 'doctype': 'Sales Invoice',
+ 'item_code': item,
+ 'company': company,
+ 'customer': si.customer,
+ 'selling_price_list': price_list,
+ 'price_list_currency': price_list_currency,
+ 'plc_conversion_rate': 1.0,
+ 'conversion_rate': 1.0
+ }
+
+ item_line = si.append('items', {})
+ item_details = get_item_details(args)
+ item_line.item_code = item
+ item_line.qty = 1
+ item_line.rate = item_details.price_list_rate
+ item_line.amount = flt(item_line.rate) * flt(item_line.qty)
+ item_line.reference_dt = 'Therapy Plan'
+ item_line.reference_dn = reference_name
+ item_line.description = item_details.description
+
+ si.set_missing_values(for_validate = True)
+ return si
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py
index df647829db..6526acda15 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py
@@ -4,10 +4,18 @@ from frappe import _
def get_data():
return {
'fieldname': 'therapy_plan',
+ 'non_standard_fieldnames': {
+ 'Sales Invoice': 'reference_dn'
+ },
'transactions': [
{
'label': _('Therapy Sessions'),
'items': ['Therapy Session']
+ },
+ {
+ 'label': _('Billing'),
+ 'items': ['Sales Invoice']
}
- ]
+ ],
+ 'disable_create_buttons': ['Sales Invoice']
}
diff --git a/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json
index 9eb20e2ef3..555587ea47 100644
--- a/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json
+++ b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json
@@ -35,7 +35,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-03-30 22:02:01.740109",
+ "modified": "2020-10-08 01:17:34.778028",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Plan Detail",
diff --git a/erpnext/healthcare/doctype/therapy_plan_template/__init__.py b/erpnext/healthcare/doctype/therapy_plan_template/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/healthcare/doctype/therapy_plan_template/test_therapy_plan_template.py b/erpnext/healthcare/doctype/therapy_plan_template/test_therapy_plan_template.py
new file mode 100644
index 0000000000..33ee29db7d
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template/test_therapy_plan_template.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestTherapyPlanTemplate(unittest.TestCase):
+ pass
diff --git a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.js b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.js
new file mode 100644
index 0000000000..86de1928e2
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.js
@@ -0,0 +1,57 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Therapy Plan Template', {
+ refresh: function(frm) {
+ frm.set_query('therapy_type', 'therapy_types', () => {
+ return {
+ filters: {
+ 'is_billable': 1
+ }
+ };
+ });
+ },
+
+ set_totals: function(frm) {
+ let total_sessions = 0;
+ let total_amount = 0.0;
+ frm.doc.therapy_types.forEach((d) => {
+ if (d.no_of_sessions) total_sessions += cint(d.no_of_sessions);
+ if (d.amount) total_amount += flt(d.amount);
+ });
+ frm.set_value('total_sessions', total_sessions);
+ frm.set_value('total_amount', total_amount);
+ frm.refresh_fields();
+ }
+});
+
+frappe.ui.form.on('Therapy Plan Template Detail', {
+ therapy_type: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ frappe.call('frappe.client.get', {
+ doctype: 'Therapy Type',
+ name: row.therapy_type
+ }).then((res) => {
+ row.rate = res.message.rate;
+ if (!row.no_of_sessions)
+ row.no_of_sessions = 1;
+ row.amount = flt(row.rate) * cint(row.no_of_sessions);
+ frm.refresh_field('therapy_types');
+ frm.trigger('set_totals');
+ });
+ },
+
+ no_of_sessions: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ row.amount = flt(row.rate) * cint(row.no_of_sessions);
+ frm.refresh_field('therapy_types');
+ frm.trigger('set_totals');
+ },
+
+ rate: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ row.amount = flt(row.rate) * cint(row.no_of_sessions);
+ frm.refresh_field('therapy_types');
+ frm.trigger('set_totals');
+ }
+});
diff --git a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.json b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.json
new file mode 100644
index 0000000000..48fc896257
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.json
@@ -0,0 +1,132 @@
+{
+ "actions": [],
+ "autoname": "field:plan_name",
+ "creation": "2020-09-22 17:51:38.861055",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "plan_name",
+ "linked_item_details_section",
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_6",
+ "description",
+ "linked_item",
+ "therapy_types_section",
+ "therapy_types",
+ "section_break_11",
+ "total_sessions",
+ "column_break_13",
+ "total_amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "plan_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Plan Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "therapy_types_section",
+ "fieldtype": "Section Break",
+ "label": "Therapy Types"
+ },
+ {
+ "fieldname": "therapy_types",
+ "fieldtype": "Table",
+ "label": "Therapy Types",
+ "options": "Therapy Plan Template Detail",
+ "reqd": 1
+ },
+ {
+ "fieldname": "linked_item",
+ "fieldtype": "Link",
+ "label": "Linked Item",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "linked_item_details_section",
+ "fieldtype": "Section Break",
+ "label": "Linked Item Details"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Data",
+ "label": "Item Code",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Item Description"
+ },
+ {
+ "fieldname": "total_amount",
+ "fieldtype": "Currency",
+ "label": "Total Amount",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "total_sessions",
+ "fieldtype": "Int",
+ "label": "Total Sessions",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-10-08 00:56:58.062105",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Therapy Plan Template",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py
new file mode 100644
index 0000000000..748c12c689
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from frappe.utils import cint, flt
+from erpnext.healthcare.doctype.therapy_type.therapy_type import make_item_price
+
+class TherapyPlanTemplate(Document):
+ def after_insert(self):
+ self.create_item_from_template()
+
+ def validate(self):
+ self.set_totals()
+
+ def on_update(self):
+ doc_before_save = self.get_doc_before_save()
+ if not doc_before_save: return
+ if doc_before_save.item_name != self.item_name or doc_before_save.item_group != self.item_group \
+ or doc_before_save.description != self.description:
+ self.update_item()
+
+ if doc_before_save.therapy_types != self.therapy_types:
+ self.update_item_price()
+
+ def set_totals(self):
+ total_sessions = 0
+ total_amount = 0
+
+ for entry in self.therapy_types:
+ total_sessions += cint(entry.no_of_sessions)
+ total_amount += flt(entry.amount)
+
+ self.total_sessions = total_sessions
+ self.total_amount = total_amount
+
+ def create_item_from_template(self):
+ uom = frappe.db.exists('UOM', 'Nos') or frappe.db.get_single_value('Stock Settings', 'stock_uom')
+
+ item = frappe.get_doc({
+ 'doctype': 'Item',
+ 'item_code': self.item_code,
+ 'item_name': self.item_name,
+ 'item_group': self.item_group,
+ 'description': self.description,
+ 'is_sales_item': 1,
+ 'is_service_item': 1,
+ 'is_purchase_item': 0,
+ 'is_stock_item': 0,
+ 'show_in_website': 0,
+ 'is_pro_applicable': 0,
+ 'stock_uom': uom
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ make_item_price(item.name, self.total_amount)
+ self.db_set('linked_item', item.name)
+
+ def update_item(self):
+ item_doc = frappe.get_doc('Item', {'item_code': self.linked_item})
+ item_doc.item_name = self.item_name
+ item_doc.item_group = self.item_group
+ item_doc.description = self.description
+ item_doc.ignore_mandatory = True
+ item_doc.save(ignore_permissions=True)
+
+ def update_item_price(self):
+ item_price = frappe.get_doc('Item Price', {'item_code': self.linked_item})
+ item_price.item_name = self.item_name
+ item_price.price_list_rate = self.total_amount
+ item_price.ignore_mandatory = True
+ item_price.save(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py
new file mode 100644
index 0000000000..c748fbfcb7
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'therapy_plan_template',
+ 'transactions': [
+ {
+ 'label': _('Therapy Plans'),
+ 'items': ['Therapy Plan']
+ }
+ ]
+ }
diff --git a/erpnext/healthcare/doctype/therapy_plan_template_detail/__init__.py b/erpnext/healthcare/doctype/therapy_plan_template_detail/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/healthcare/doctype/therapy_plan_template_detail/therapy_plan_template_detail.json b/erpnext/healthcare/doctype/therapy_plan_template_detail/therapy_plan_template_detail.json
new file mode 100644
index 0000000000..5553a118f8
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template_detail/therapy_plan_template_detail.json
@@ -0,0 +1,54 @@
+{
+ "actions": [],
+ "creation": "2020-10-07 23:04:44.373381",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "therapy_type",
+ "no_of_sessions",
+ "rate",
+ "amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "therapy_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Therapy Type",
+ "options": "Therapy Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "no_of_sessions",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "No of Sessions"
+ },
+ {
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "read_only": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-10-07 23:46:54.296322",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Therapy Plan Template Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/therapy_plan_template_detail/therapy_plan_template_detail.py b/erpnext/healthcare/doctype/therapy_plan_template_detail/therapy_plan_template_detail.py
new file mode 100644
index 0000000000..7b979fe9fc
--- /dev/null
+++ b/erpnext/healthcare/doctype/therapy_plan_template_detail/therapy_plan_template_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class TherapyPlanTemplateDetail(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
index e66e66751a..65d4cc4861 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
@@ -92,7 +92,8 @@ frappe.ui.form.on('Therapy Session', {
'start_date': data.message.appointment_date,
'start_time': data.message.appointment_time,
'service_unit': data.message.service_unit,
- 'company': data.message.company
+ 'company': data.message.company,
+ 'duration': data.message.duration
};
frm.set_value(values);
}
@@ -107,6 +108,7 @@ frappe.ui.form.on('Therapy Session', {
'start_date': '',
'start_time': '',
'service_unit': '',
+ 'duration': ''
};
frm.set_value(values);
}
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json
index dc0cafcf9c..1f877cc296 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.json
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json
@@ -47,7 +47,8 @@
"fieldname": "appointment",
"fieldtype": "Link",
"label": "Appointment",
- "options": "Patient Appointment"
+ "options": "Patient Appointment",
+ "set_only_once": 1
},
{
"fieldname": "patient",
@@ -90,7 +91,8 @@
"fetch_from": "therapy_template.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
- "label": "Duration"
+ "label": "Duration",
+ "reqd": 1
},
{
"fieldname": "location",
@@ -220,7 +222,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-06-30 10:56:10.354268",
+ "modified": "2020-10-22 23:10:21.178644",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Therapy Session",
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
index 9650183712..85d0970177 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
@@ -4,16 +4,41 @@
from __future__ import unicode_literals
import frappe
+import datetime
from frappe.model.document import Document
+from frappe.utils import get_time, flt
from frappe.model.mapper import get_mapped_doc
from frappe import _
-from frappe.utils import cstr, getdate
+from frappe.utils import cstr, getdate, get_link_to_form
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account
class TherapySession(Document):
def validate(self):
+ self.validate_duplicate()
self.set_total_counts()
+ def validate_duplicate(self):
+ end_time = datetime.datetime.combine(getdate(self.start_date), get_time(self.start_time)) \
+ + datetime.timedelta(minutes=flt(self.duration))
+
+ overlaps = frappe.db.sql("""
+ select
+ name
+ from
+ `tabTherapy Session`
+ where
+ start_date=%s and name!=%s and docstatus!=2
+ and (practitioner=%s or patient=%s) and
+ ((start_time<%s and start_time + INTERVAL duration MINUTE>%s) or
+ (start_time>%s and start_time<%s) or
+ (start_time=%s))
+ """, (self.start_date, self.name, self.practitioner, self.patient,
+ self.start_time, end_time.time(), self.start_time, end_time.time(), self.start_time))
+
+ if overlaps:
+ overlapping_details = _('Therapy Session overlaps with {0}').format(get_link_to_form('Therapy Session', overlaps[0][0]))
+ frappe.throw(overlapping_details, title=_('Therapy Sessions Overlapping'))
+
def on_submit(self):
self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)
diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.py b/erpnext/healthcare/doctype/therapy_type/therapy_type.py
index ea3d84e7c5..6c825b8a58 100644
--- a/erpnext/healthcare/doctype/therapy_type/therapy_type.py
+++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.py
@@ -41,7 +41,7 @@ class TherapyType(Document):
if self.rate:
item_price = frappe.get_doc('Item Price', {'item_code': self.item})
item_price.item_name = self.item_name
- item_price.price_list_name = self.rate
+ item_price.price_list_rate = self.rate
item_price.ignore_mandatory = True
item_price.save()
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index dbd3b83f09..96282f50a9 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -23,9 +23,9 @@ def get_healthcare_services_to_invoice(patient, company):
items_to_invoice += get_lab_tests_to_invoice(patient, company)
items_to_invoice += get_clinical_procedures_to_invoice(patient, company)
items_to_invoice += get_inpatient_services_to_invoice(patient, company)
+ items_to_invoice += get_therapy_plans_to_invoice(patient, company)
items_to_invoice += get_therapy_sessions_to_invoice(patient, company)
-
return items_to_invoice
@@ -35,6 +35,7 @@ def validate_customer_created(patient):
msg += " {0}".format(patient.name)
frappe.throw(msg, title=_('Customer Not Found'))
+
def get_appointments_to_invoice(patient, company):
appointments_to_invoice = []
patient_appointments = frappe.get_list(
@@ -246,12 +247,44 @@ def get_inpatient_services_to_invoice(patient, company):
return services_to_invoice
+def get_therapy_plans_to_invoice(patient, company):
+ therapy_plans_to_invoice = []
+ therapy_plans = frappe.get_list(
+ 'Therapy Plan',
+ fields=['therapy_plan_template', 'name'],
+ filters={
+ 'patient': patient.name,
+ 'invoiced': 0,
+ 'company': company,
+ 'therapy_plan_template': ('!=', '')
+ }
+ )
+ for plan in therapy_plans:
+ therapy_plans_to_invoice.append({
+ 'reference_type': 'Therapy Plan',
+ 'reference_name': plan.name,
+ 'service': frappe.db.get_value('Therapy Plan Template', plan.therapy_plan_template, 'linked_item')
+ })
+
+ return therapy_plans_to_invoice
+
+
def get_therapy_sessions_to_invoice(patient, company):
therapy_sessions_to_invoice = []
+ therapy_plans = frappe.db.get_all('Therapy Plan', {'therapy_plan_template': ('!=', '')})
+ therapy_plans_created_from_template = []
+ for entry in therapy_plans:
+ therapy_plans_created_from_template.append(entry.name)
+
therapy_sessions = frappe.get_list(
'Therapy Session',
fields='*',
- filters={'patient': patient.name, 'invoiced': 0, 'company': company}
+ filters={
+ 'patient': patient.name,
+ 'invoiced': 0,
+ 'company': company,
+ 'therapy_plan': ('not in', therapy_plans_created_from_template)
+ }
)
for therapy in therapy_sessions:
if not therapy.appointment:
@@ -368,8 +401,8 @@ def validate_invoiced_on_submit(item):
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
if is_invoiced:
- frappe.throw(_('The item referenced by {0} - {1} is already invoiced'\
- ).format(item.reference_dt, item.reference_dn))
+ frappe.throw(_('The item referenced by {0} - {1} is already invoiced').format(
+ item.reference_dt, item.reference_dn))
def manage_prescriptions(invoiced, ref_dt, ref_dn, dt, created_check_field):
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9bd31050c4..dbb6c0d92e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -282,7 +282,8 @@ doc_events = {
# to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled.
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
auto_cancel_exempted_doctypes= [
- "Payment Entry"
+ "Payment Entry",
+ "Inpatient Medication Entry"
]
scheduler_events = {
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 5b45c22123..8b34eaa0a8 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -731,3 +731,4 @@ erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
+erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
diff --git a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py
new file mode 100644
index 0000000000..585e540626
--- /dev/null
+++ b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py
@@ -0,0 +1,10 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from erpnext.domains.healthcare import data
+
+def execute():
+ if 'Healthcare' not in frappe.get_active_domains():
+ return
+
+ if data['custom_fields']:
+ create_custom_fields(data['custom_fields'])
\ No newline at end of file
diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css
index e80e3ed126..47f577131a 100644
--- a/erpnext/public/css/pos.css
+++ b/erpnext/public/css/pos.css
@@ -210,6 +210,7 @@
[data-route="point-of-sale"] .item-summary-wrapper:last-child { border-bottom: none; }
[data-route="point-of-sale"] .total-summary-wrapper:last-child { border-bottom: none; }
[data-route="point-of-sale"] .invoices-container .invoice-wrapper:last-child { border-bottom: none; }
+[data-route="point-of-sale"] .new-btn { background-color: #5e64ff; color: white; border: none;}
[data-route="point-of-sale"] .summary-btns:last-child { margin-right: 0px; }
[data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px }
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index be30086d34..99f3995a66 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -6,6 +6,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
apply_pricing_rule_on_item: function(item){
let effective_item_rate = item.price_list_rate;
+ let item_rate = item.rate;
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
effective_item_rate = item.blanket_order_rate;
}
@@ -17,15 +18,17 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}
item.base_rate_with_margin = flt(item.rate_with_margin) * flt(this.frm.doc.conversion_rate);
- item.rate = flt(item.rate_with_margin , precision("rate", item));
+ item_rate = flt(item.rate_with_margin , precision("rate", item));
if(item.discount_percentage){
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
if (item.discount_amount) {
- item.rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
+ item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
}
+
+ frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
},
calculate_taxes_and_totals: function(update_paid_amount) {
@@ -88,11 +91,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
if(this.frm.doc.currency == company_currency) {
this.frm.set_value("conversion_rate", 1);
} else {
- const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', [
- conversion_rate_label,
- this.frm.doc.currency,
- company_currency
- ]);
+ const subs = [conversion_rate_label, this.frm.doc.currency, company_currency];
+ const err_message = __('{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}', subs);
frappe.throw(err_message);
}
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 33911793f6..23705a8779 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1049,14 +1049,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(item.item_code && item.uom) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_conversion_factor",
- child: item,
args: {
item_code: item.item_code,
uom: item.uom
},
callback: function(r) {
if(!r.exc) {
- me.conversion_factor(me.frm.doc, cdt, cdn);
+ frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
}
}
});
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index d9f6e1d433..2623c3c1a7 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'),
- default: 0
+ default: flt(me.item.stock_qty),
},
{
fieldname: 'auto_fetch_button',
@@ -91,7 +91,8 @@ erpnext.SerialNoBatchSelector = Class.extend({
qty: qty,
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
- batch_no: me.item.batch_no || null
+ batch_no: me.item.batch_no || null,
+ posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
}
});
@@ -100,11 +101,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
let records_length = auto_fetched_serial_numbers.length;
if (!records_length) {
const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
- frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
- under warehouse ${warehouse}. Please try changing warehouse.`));
+ frappe.msgprint(
+ __('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [me.item.item_code.bold(), warehouse])
+ );
}
if (records_length < qty) {
- frappe.msgprint(__(`Fetched only ${records_length} available serial numbers.`));
+ frappe.msgprint(__('Fetched only {0} available serial numbers.', [records_length]));
}
let serial_no_list_field = this.dialog.fields_dict.serial_no;
numbers = auto_fetched_serial_numbers.join('\n');
@@ -189,15 +191,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
}
if(this.has_batch && !this.has_serial_no) {
if(values.batches.length === 0 || !values.batches) {
- frappe.throw(__("Please select batches for batched item "
- + values.item_code));
- return false;
+ frappe.throw(__("Please select batches for batched item {0}", [values.item_code]));
}
values.batches.map((batch, i) => {
if(!batch.selected_qty || batch.selected_qty === 0 ) {
if (!this.show_dialog) {
- frappe.throw(__("Please select quantity on row " + (i+1)));
- return false;
+ frappe.throw(__("Please select quantity on row {0}", [i+1]));
}
}
});
@@ -206,9 +205,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
} else {
let serial_nos = values.serial_no || '';
if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
- frappe.throw(__("Please enter serial numbers for serialized item "
- + values.item_code));
- return false;
+ frappe.throw(__("Please enter serial numbers for serialized item {0}", [values.item_code]));
}
return true;
}
@@ -355,8 +352,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
});
if (selected_batches.includes(val)) {
this.set_value("");
- frappe.throw(__(`Batch ${val} already selected.`));
- return;
+ frappe.throw(__('Batch {0} already selected.', [val]));
}
if (me.warehouse_details.name) {
@@ -375,8 +371,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
} else {
this.set_value("");
- frappe.throw(__(`Please select a warehouse to get available
- quantities`));
+ frappe.throw(__('Please select a warehouse to get available quantities'));
}
// e.stopImmediatePropagation();
}
@@ -411,8 +406,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
parseFloat(available_qty) < parseFloat(selected_qty)) {
this.set_value('0');
- frappe.throw(__(`For transfer from source, selected quantity cannot be
- greater than available quantity`));
+ frappe.throw(__('For transfer from source, selected quantity cannot be greater than available quantity'));
} else {
this.grid.refresh();
}
@@ -451,20 +445,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos",
args: {
- item_code: me.item_code,
- warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : ''
+ filters: {
+ item_code: me.item_code,
+ warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
+ }
}
}).then((data) => {
- if (!data.message[1].length) {
- this.showing_reserved_serial_nos_error = true;
- const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
- const d = frappe.msgprint(__(`Serial numbers unavailable for Item ${me.item.item_code.bold()}
- under warehouse ${warehouse}. Please try changing warehouse.`));
- d.get_close_btn().on('click', () => {
- this.showing_reserved_serial_nos_error = false;
- d.hide();
- });
- }
serial_no_filters['name'] = ["not in", data.message[0]]
})
}
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 9e25ed0c99..a33d401b57 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -441,6 +441,7 @@ class TestSalesOrder(unittest.TestCase):
def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow
+ frappe.set_user("Administrator")
workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
apply_workflow(so, 'Approve')
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js
index 8d4ac78422..9d44a9f862 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.js
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.js
@@ -12,4 +12,12 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) {
wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
window.cur_pos = wrapper.pos;
-};
\ No newline at end of file
+};
+
+frappe.pages['point-of-sale'].refresh = function(wrapper) {
+ if (document.scannerDetectionData) {
+ onScan.detachFrom(document);
+ wrapper.pos.wrapper.html("");
+ wrapper.pos.check_opening_entry();
+ }
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 83bd71d5f3..e5b50d7789 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -11,54 +11,67 @@ from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availabil
from six import string_types
@frappe.whitelist()
-def get_items(start, page_length, price_list, item_group, search_value="", pos_profile=None):
+def get_items(start, page_length, price_list, item_group, pos_profile, search_value=""):
data = dict()
- warehouse = ""
+ result = []
+ warehouse, show_only_available_items = "", False
- if pos_profile:
- warehouse = frappe.db.get_value('POS Profile', pos_profile, ['warehouse'])
+ allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
+ if not allow_negative_stock:
+ warehouse, show_only_available_items = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'show_only_available_items'])
if not frappe.db.exists('Item Group', item_group):
item_group = get_root_of('Item Group')
if search_value:
data = search_serial_or_batch_or_barcode_number(search_value)
-
+
item_code = data.get("item_code") if data.get("item_code") else search_value
serial_no = data.get("serial_no") if data.get("serial_no") else ""
batch_no = data.get("batch_no") if data.get("batch_no") else ""
barcode = data.get("barcode") if data.get("barcode") else ""
- condition = get_conditions(item_code, serial_no, batch_no, barcode)
+ if data:
+ item_info = frappe.db.get_value(
+ "Item", data.get("item_code"),
+ ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"]
+ , as_dict=1)
+ item_info.setdefault('serial_no', serial_no)
+ item_info.setdefault('batch_no', batch_no)
+ item_info.setdefault('barcode', barcode)
- if pos_profile:
- condition += get_item_group_condition(pos_profile)
+ return { 'items': [item_info] }
+
+ condition = get_conditions(item_code, serial_no, batch_no, barcode)
+ condition += get_item_group_condition(pos_profile)
lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
- # locate function is used to sort by closest match from the beginning of the value
- result = []
+ bin_join_selection, bin_join_condition = "", ""
+ if show_only_available_items:
+ bin_join_selection = ", `tabBin` bin"
+ bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0"
items_data = frappe.db.sql("""
SELECT
- name AS item_code,
- item_name,
- description,
- stock_uom,
- image AS item_image,
- idx AS idx,
- is_stock_item
+ item.name AS item_code,
+ item.item_name,
+ item.description,
+ item.stock_uom,
+ item.image AS item_image,
+ item.is_stock_item
FROM
- `tabItem`
+ `tabItem` item {bin_join_selection}
WHERE
- disabled = 0
- AND has_variants = 0
- AND is_sales_item = 1
- AND is_fixed_asset = 0
- AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
- AND {condition}
+ item.disabled = 0
+ AND item.has_variants = 0
+ AND item.is_sales_item = 1
+ AND item.is_fixed_asset = 0
+ AND item.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
+ AND {condition}
+ {bin_join_condition}
ORDER BY
- name asc
+ item.name asc
LIMIT
{start}, {page_length}"""
.format(
@@ -66,8 +79,10 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
page_length=page_length,
lft=lft,
rgt=rgt,
- condition=condition
- ), as_dict=1)
+ condition=condition,
+ bin_join_selection=bin_join_selection,
+ bin_join_condition=bin_join_condition
+ ), {'warehouse': warehouse}, as_dict=1)
if items_data:
items = [d.item_code for d in items_data]
@@ -82,46 +97,24 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
for item in items_data:
item_code = item.item_code
item_price = item_prices.get(item_code) or {}
- item_stock_qty = get_stock_availability(item_code, warehouse)
-
- if not item_stock_qty:
- pass
+ if not allow_negative_stock:
+ item_stock_qty = frappe.db.sql("""select ifnull(sum(actual_qty), 0) from `tabBin` where item_code = %s""", item_code)[0][0]
else:
- row = {}
- row.update(item)
- row.update({
- 'price_list_rate': item_price.get('price_list_rate'),
- 'currency': item_price.get('currency'),
- 'actual_qty': item_stock_qty,
- })
- result.append(row)
+ item_stock_qty = get_stock_availability(item_code, warehouse)
+
+ row = {}
+ row.update(item)
+ row.update({
+ 'price_list_rate': item_price.get('price_list_rate'),
+ 'currency': item_price.get('currency'),
+ 'actual_qty': item_stock_qty,
+ })
+ result.append(row)
res = {
'items': result
}
- if len(res['items']) == 1:
- res['items'][0].setdefault('serial_no', serial_no)
- res['items'][0].setdefault('batch_no', batch_no)
- res['items'][0].setdefault('barcode', barcode)
-
- return res
-
- if serial_no:
- res.update({
- 'serial_no': serial_no
- })
-
- if batch_no:
- res.update({
- 'batch_no': batch_no
- })
-
- if barcode:
- res.update({
- 'barcode': barcode
- })
-
return res
@frappe.whitelist()
@@ -145,16 +138,16 @@ def search_serial_or_batch_or_barcode_number(search_value):
def get_conditions(item_code, serial_no, batch_no, barcode):
if serial_no or batch_no or barcode:
- return "name = {0}".format(frappe.db.escape(item_code))
+ return "item.name = {0}".format(frappe.db.escape(item_code))
- return """(name like {item_code}
- or item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
+ return """(item.name like {item_code}
+ or item.item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
def get_item_group_condition(pos_profile):
cond = "and 1=1"
item_groups = get_item_groups(pos_profile)
if item_groups:
- cond = "and item_group in (%s)"%(', '.join(['%s']*len(item_groups)))
+ cond = "and item.item_group in (%s)"%(', '.join(['%s']*len(item_groups)))
return cond % tuple(item_groups)
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 5018254b0a..3d0054647b 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -20,27 +20,58 @@ erpnext.PointOfSale.Controller = class {
frappe.require(['assets/erpnext/css/pos.css'], this.check_opening_entry.bind(this));
}
+ fetch_opening_entry() {
+ return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user });
+ }
+
check_opening_entry() {
- return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.check_opening_entry", { "user": frappe.session.user })
- .then((r) => {
- if (r.message.length) {
- // assuming only one opening voucher is available for the current user
- this.prepare_app_defaults(r.message[0]);
- } else {
- this.create_opening_voucher();
- }
- });
+ this.fetch_opening_entry().then((r) => {
+ if (r.message.length) {
+ // assuming only one opening voucher is available for the current user
+ this.prepare_app_defaults(r.message[0]);
+ } else {
+ this.create_opening_voucher();
+ }
+ });
}
create_opening_voucher() {
const table_fields = [
- { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 },
- { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount",
- options: "company:company_currency" }
+ {
+ fieldname: "mode_of_payment", fieldtype: "Link",
+ in_list_view: 1, label: "Mode of Payment",
+ options: "Mode of Payment", reqd: 1
+ },
+ {
+ fieldname: "opening_amount", fieldtype: "Currency",
+ in_list_view: 1, label: "Opening Amount",
+ options: "company:company_currency",
+ change: function () {
+ dialog.fields_dict.balance_details.df.data.some(d => {
+ if (d.idx == this.doc.idx) {
+ d.opening_amount = this.value;
+ dialog.fields_dict.balance_details.grid.refresh();
+ return true;
+ }
+ });
+ }
+ }
];
-
+ const fetch_pos_payment_methods = () => {
+ const pos_profile = dialog.fields_dict.pos_profile.get_value();
+ if (!pos_profile) return;
+ frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => {
+ dialog.fields_dict.balance_details.df.data = [];
+ payments.forEach(pay => {
+ const { mode_of_payment } = pay;
+ dialog.fields_dict.balance_details.df.data.push({ mode_of_payment, opening_amount: '0' });
+ });
+ dialog.fields_dict.balance_details.grid.refresh();
+ });
+ }
const dialog = new frappe.ui.Dialog({
title: __('Create POS Opening Entry'),
+ static: true,
fields: [
{
fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'),
@@ -49,20 +80,7 @@ erpnext.PointOfSale.Controller = class {
{
fieldtype: 'Link', label: __('POS Profile'),
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
- onchange: () => {
- const pos_profile = dialog.fields_dict.pos_profile.get_value();
-
- if (!pos_profile) return;
-
- frappe.db.get_doc("POS Profile", pos_profile).then(doc => {
- dialog.fields_dict.balance_details.df.data = [];
- doc.payments.forEach(pay => {
- const { mode_of_payment } = pay;
- dialog.fields_dict.balance_details.df.data.push({ mode_of_payment });
- });
- dialog.fields_dict.balance_details.grid.refresh();
- });
- }
+ onchange: () => fetch_pos_payment_methods()
},
{
fieldname: "balance_details",
@@ -75,25 +93,18 @@ erpnext.PointOfSale.Controller = class {
fields: table_fields
}
],
- primary_action: ({ company, pos_profile, balance_details }) => {
+ primary_action: async ({ company, pos_profile, balance_details }) => {
if (!balance_details.length) {
frappe.show_alert({
message: __("Please add Mode of payments and opening balance details."),
indicator: 'red'
})
- frappe.utils.play_sound("error");
- return;
+ return frappe.utils.play_sound("error");
}
- frappe.dom.freeze();
- return frappe.call("erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher",
- { pos_profile, company, balance_details })
- .then((r) => {
- frappe.dom.unfreeze();
- dialog.hide();
- if (r.message) {
- this.prepare_app_defaults(r.message);
- }
- })
+ const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher";
+ const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true });
+ !res.exc && this.prepare_app_defaults(res.message);
+ dialog.hide();
},
primary_action_label: __('Submit')
});
@@ -145,8 +156,8 @@ erpnext.PointOfSale.Controller = class {
}
prepare_dom() {
- this.wrapper.append(`
-