Merge branch 'develop' into quality-inspection-parameter-group
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94573f9..76e0092 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -179,10 +179,18 @@
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")]
- })
+ serial_no_exists = frappe.db.sql("""
+ SELECT name
+ FROM `tabPOS Invoice Item`
+ WHERE
+ parent = %s
+ and (serial_no = %s
+ or serial_no like %s
+ or serial_no like %s
+ or serial_no like %s
+ )
+ """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%'))
+
if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr)
@@ -190,7 +198,7 @@
_("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_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
@@ -292,7 +300,7 @@
if not self.get('payments') and not for_validate:
update_multi_mode_option(self, profile)
-
+
if self.is_return and not for_validate:
add_return_modes(self, profile)
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 57a23af..15875af 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -198,6 +198,65 @@
self.assertEqual(pos_return.get('payments')[0].amount, -500)
self.assertEqual(pos_return.get('payments')[1].amount, -500)
+ def test_pos_return_for_serialized_item(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(company='_Test Company',
+ target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+ account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+ expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+ item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+
+ pos.get("items")[0].serial_no = serial_nos[0]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return = make_sales_return(pos.name)
+
+ pos_return.insert()
+ pos_return.submit()
+ self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0])
+
+ def test_partial_pos_returns(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(company='_Test Company',
+ target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+ account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+ expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+ item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1)
+
+ pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return1 = make_sales_return(pos.name)
+
+ # partial return 1
+ pos_return1.get('items')[0].qty = -1
+ pos_return1.get('items')[0].serial_no = serial_nos[0]
+ pos_return1.insert()
+ pos_return1.submit()
+
+ # partial return 2
+ pos_return2 = make_sales_return(pos.name)
+ self.assertEqual(pos_return2.get('items')[0].qty, -1)
+ self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1])
+
def test_pos_change_amount(self):
pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 2b6e7de..8b71eb0 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -87,6 +87,7 @@
"edit_references",
"sales_order",
"so_detail",
+ "pos_invoice_item",
"column_break_74",
"delivery_note",
"dn_detail",
@@ -790,11 +791,20 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "fieldname": "pos_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "POS Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-07-22 13:40:34.418346",
+ "modified": "2021-01-04 17:34:49.924531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
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 c88d679..40f77b4 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
@@ -29,7 +29,7 @@
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:
@@ -58,7 +58,7 @@
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
-
+
if returns:
credit_note = self.process_merging_into_credit_note(returns)
@@ -74,7 +74,7 @@
def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice()
-
+
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
@@ -98,19 +98,19 @@
self.consolidated_credit_note = credit_note.name
return credit_note.name
-
+
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
-
+
if doc.redeem_loyalty_points:
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
loyalty_points_sum += doc.loyalty_points
loyalty_amount_sum += doc.loyalty_amount
-
+
for item in doc.get('items'):
found = False
for i in items:
@@ -118,12 +118,13 @@
i.uom == item.uom and i.net_rate == item.net_rate):
found = True
i.qty = i.qty + item.qty
+
if not found:
item.rate = item.net_rate
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
-
+
for tax in doc.get('taxes'):
found = False
for t in taxes:
@@ -162,7 +163,7 @@
invoice.ignore_pricing_rule = 1
return invoice
-
+
def get_new_sales_invoice(self):
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
@@ -194,7 +195,7 @@
}
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
-
+
return pos_invoices
def get_invoice_customer_map(pos_invoices):
@@ -204,7 +205,7 @@
customer = invoice.get('customer')
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
-
+
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index d011689..76f3c50 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -222,7 +222,7 @@
set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters,
- gl_entries_by_account, accounts_by_name, ignore_closing_entries=False)
+ gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
@@ -339,7 +339,7 @@
return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account,
- accounts_by_name, ignore_closing_entries=False):
+ accounts_by_name, accounts, ignore_closing_entries=False):
"""Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company',
@@ -382,15 +382,31 @@
for entry in gl_entries:
key = entry.account_number or entry.account_name
- validate_entries(key, entry, accounts_by_name)
+ validate_entries(key, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry)
return gl_entries_by_account
-def validate_entries(key, entry, accounts_by_name):
+def get_account_details(account):
+ return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company',
+ 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
+
+def validate_entries(key, entry, accounts_by_name, accounts):
if key not in accounts_by_name:
- field = "Account number" if entry.account_number else "Account name"
- frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
+ args = get_account_details(entry.account)
+
+ if args.parent_account:
+ parent_args = get_account_details(args.parent_account)
+
+ args.update({
+ 'lft': parent_args.lft + 1,
+ 'rgt': parent_args.rgt - 1,
+ 'root_type': parent_args.root_type,
+ 'report_type': parent_args.report_type
+ })
+
+ accounts_by_name.setdefault(key, args)
+ accounts.append(args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 40362b1..4cc5753 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -26,7 +26,6 @@
"supplier_group",
"supplier_type",
"pan",
- "language",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"disabled",
@@ -57,6 +56,7 @@
"website",
"supplier_details",
"column_break_30",
+ "language",
"is_frozen"
],
"fields": [
@@ -384,7 +384,7 @@
"idx": 370,
"image_field": "image",
"links": [],
- "modified": "2020-06-17 23:18:20",
+ "modified": "2021-01-06 19:51:40.939087",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 0e1829a..de61b35 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -204,8 +204,6 @@
return items
def get_returned_qty_map_for_row(row_name, doctype):
- if doctype == "POS Invoice": return {}
-
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
@@ -354,7 +352,12 @@
target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
- target_doc.sales_invoice_item = source_doc.name
+
+ if doctype == "Sales Invoice":
+ target_doc.sales_invoice_item = source_doc.name
+ else:
+ target_doc.pos_invoice_item = source_doc.name
+
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 2df1793..1b33fd7 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -49,6 +49,7 @@
"phone",
"mobile_no",
"fax",
+ "website",
"more_info",
"type",
"market_segment",
@@ -56,8 +57,8 @@
"request_type",
"column_break3",
"company",
- "website",
"territory",
+ "language",
"unsubscribed",
"blog_subscriber",
"title"
@@ -447,13 +448,19 @@
"fieldtype": "Select",
"label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther"
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2020-10-13 15:24:00.094811",
+ "modified": "2021-01-06 19:39:58.748978",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index eee13f7..2e09a76 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -54,6 +54,7 @@
"campaign",
"column_break1",
"transaction_date",
+ "language",
"amended_from",
"lost_reasons"
],
@@ -419,12 +420,18 @@
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2020-08-12 17:34:35.066961",
+ "modified": "2021-01-06 19:42:46.190051",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
index 15916a5..861675a 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
@@ -2,4 +2,82 @@
// For license information, please see license.txt
frappe.ui.form.on('Appointment Type', {
+ refresh: function(frm) {
+ frm.set_query('price_list', function() {
+ return {
+ filters: {'selling': 1}
+ };
+ });
+
+ frm.set_query('medical_department', 'items', function(doc) {
+ let item_list = doc.items.map(({medical_department}) => medical_department);
+ return {
+ filters: [
+ ['Medical Department', 'name', 'not in', item_list]
+ ]
+ };
+ });
+
+ frm.set_query('op_consulting_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+
+ frm.set_query('inpatient_visit_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+ }
});
+
+frappe.ui.form.on('Appointment Type Service Item', {
+ op_consulting_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.op_consulting_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.op_consulting_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function(data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ },
+
+ inpatient_visit_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.inpatient_visit_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.inpatient_visit_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function (data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.json b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
index 58753bb..3872318 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.json
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
@@ -12,7 +12,10 @@
"appointment_type",
"ip",
"default_duration",
- "color"
+ "color",
+ "billing_section",
+ "price_list",
+ "items"
],
"fields": [
{
@@ -52,10 +55,27 @@
"label": "Color",
"no_copy": 1,
"report_hide": 1
+ },
+ {
+ "fieldname": "billing_section",
+ "fieldtype": "Section Break",
+ "label": "Billing"
+ },
+ {
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Appointment Type Service Items",
+ "options": "Appointment Type Service Item"
}
],
"links": [],
- "modified": "2020-02-03 21:06:05.833050",
+ "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
index 1dacffa..67a24f3 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
@@ -4,6 +4,53 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+import frappe
class AppointmentType(Document):
- pass
+ def validate(self):
+ if self.items and self.price_list:
+ for item in self.items:
+ existing_op_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.op_consulting_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
+ make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
+
+ existing_ip_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.inpatient_visit_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
+ make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
+
+@frappe.whitelist()
+def get_service_item_based_on_department(appointment_type, department):
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'medical_department': department, 'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ # if department wise items are not set up
+ # use the generic items
+ if not item_list:
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ return item_list
+
+def make_item_price(price_list, item, item_price):
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'price_list': price_list,
+ 'item_code': item,
+ 'price_list_rate': item_price
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
new file mode 100644
index 0000000..5ff68cd
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
@@ -0,0 +1,67 @@
+{
+ "actions": [],
+ "creation": "2021-01-22 09:34:53.373105",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "medical_department",
+ "op_consulting_charge_item",
+ "op_consulting_charge",
+ "column_break_4",
+ "inpatient_visit_charge_item",
+ "inpatient_visit_charge"
+ ],
+ "fields": [
+ {
+ "fieldname": "medical_department",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "fieldname": "op_consulting_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "op_consulting_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "inpatient_visit_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "inpatient_visit_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-01-22 09:35:26.503443",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Appointment Type Service Item",
+ "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/appointment_type_service_item/appointment_type_service_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
new file mode 100644
index 0000000..b2e0e82
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, 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 AppointmentTypeServiceItem(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
index cb747f9..8162f03 100644
--- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
+++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"label": "Out Patient Consulting Charge",
+ "mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency"
},
{
@@ -174,7 +175,8 @@
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
- "label": "Inpatient Visit Charge"
+ "label": "Inpatient Visit Charge",
+ "mandatory_depends_on": "inpatient_visit_charge_item"
},
{
"depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-04-06 13:44:24.759623",
+ "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 3d5073b..0354733 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -24,11 +24,13 @@
});
frm.set_query('practitioner', function() {
- return {
- filters: {
- 'department': frm.doc.department
- }
- };
+ if (frm.doc.department) {
+ return {
+ filters: {
+ 'department': frm.doc.department
+ }
+ };
+ }
});
frm.set_query('service_unit', function() {
@@ -140,6 +142,20 @@
patient: function(frm) {
if (frm.doc.patient) {
frm.trigger('toggle_payment_fields');
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Patient',
+ name: frm.doc.patient
+ },
+ callback: function (data) {
+ let age = null;
+ if (data.message.dob) {
+ age = calculate_age(data.message.dob);
+ }
+ frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age);
+ }
+ });
} else {
frm.set_value('patient_name', '');
frm.set_value('patient_sex', '');
@@ -148,6 +164,37 @@
}
},
+ practitioner: function(frm) {
+ if (frm.doc.practitioner ) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ appointment_type: function(frm) {
+ if (frm.doc.appointment_type) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ set_payment_details: function(frm) {
+ frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => {
+ if (val) {
+ frappe.call({
+ method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge',
+ args: {
+ doc: frm.doc
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge);
+ frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item);
+ }
+ }
+ });
+ }
+ });
+ },
+
therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter');
},
@@ -190,14 +237,18 @@
// show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0);
+ frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
+ frm.toggle_reqd('billing_item', 0);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', data.message ? 1 : 0);
+ frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
+ frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
});
@@ -540,57 +591,6 @@
);
};
-frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) {
- if (frm.doc.practitioner) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Healthcare Practitioner',
- name: frm.doc.practitioner
- },
- callback: function (data) {
- frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department);
- frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge);
- frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'patient', function(frm) {
- if (frm.doc.patient) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Patient',
- name: frm.doc.patient
- },
- callback: function (data) {
- let age = null;
- if (data.message.dob) {
- age = calculate_age(data.message.dob);
- }
- frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) {
- if (frm.doc.appointment_type) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Appointment Type',
- name: frm.doc.appointment_type
- },
- callback: function(data) {
- frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration);
- }
- });
- }
-});
-
let calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date();
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 35600e4..83c92af 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -19,19 +19,19 @@
"inpatient_record",
"column_break_1",
"company",
+ "practitioner",
+ "practitioner_name",
+ "department",
"service_unit",
+ "section_break_12",
+ "appointment_type",
+ "duration",
"procedure_template",
"get_procedure_from_encounter",
"procedure_prescription",
"therapy_plan",
"therapy_type",
"get_prescribed_therapies",
- "practitioner",
- "practitioner_name",
- "department",
- "section_break_12",
- "appointment_type",
- "duration",
"column_break_17",
"appointment_date",
"appointment_time",
@@ -79,6 +79,7 @@
"set_only_once": 1
},
{
+ "fetch_from": "appointment_type.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
"in_filter": 1,
@@ -144,7 +145,6 @@
"in_standard_filter": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
- "read_only": 1,
"reqd": 1,
"search_index": 1,
"set_only_once": 1
@@ -158,7 +158,6 @@
"in_standard_filter": 1,
"label": "Department",
"options": "Medical Department",
- "read_only": 1,
"search_index": 1,
"set_only_once": 1
},
@@ -227,12 +226,14 @@
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
- "options": "Mode of Payment"
+ "options": "Mode of Payment",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
- "label": "Paid Amount"
+ "label": "Paid Amount",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "column_break_2",
@@ -302,7 +303,8 @@
"fieldname": "therapy_plan",
"fieldtype": "Link",
"label": "Therapy Plan",
- "options": "Therapy Plan"
+ "options": "Therapy Plan",
+ "set_only_once": 1
},
{
"fieldname": "ref_sales_invoice",
@@ -347,7 +349,7 @@
}
],
"links": [],
- "modified": "2020-12-16 13:16:58.578503",
+ "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index f2b94b8..1f76cd6 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -26,6 +26,7 @@
def after_insert(self):
self.update_prescription_details()
+ self.set_payment_details()
invoice_appointment(self)
self.update_fee_validity()
send_confirmation_msg(self)
@@ -85,6 +86,13 @@
def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
+ def set_payment_details(self):
+ if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
+ details = get_service_item_and_practitioner_charge(self)
+ self.db_set('billing_item', details.get('service_item'))
+ if not self.paid_amount:
+ self.db_set('paid_amount', details.get('practitioner_charge'))
+
def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'):
@@ -148,31 +156,37 @@
fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity:
- sales_invoice = frappe.new_doc('Sales Invoice')
- sales_invoice.patient = appointment_doc.patient
- sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
- sales_invoice.appointment = appointment_doc.name
- sales_invoice.due_date = getdate()
- sales_invoice.company = appointment_doc.company
- sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
+ create_sales_invoice(appointment_doc)
- item = sales_invoice.append('items', {})
- item = get_appointment_item(appointment_doc, item)
- # Add payments if payment details are supplied else proceed to create invoice as Unpaid
- if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
- sales_invoice.is_pos = 1
- payment = sales_invoice.append('payments', {})
- payment.mode_of_payment = appointment_doc.mode_of_payment
- payment.amount = appointment_doc.paid_amount
+def create_sales_invoice(appointment_doc):
+ sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice.patient = appointment_doc.patient
+ sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
+ sales_invoice.appointment = appointment_doc.name
+ sales_invoice.due_date = getdate()
+ sales_invoice.company = appointment_doc.company
+ sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
- sales_invoice.set_missing_values(for_validate=True)
- sales_invoice.flags.ignore_mandatory = True
- sales_invoice.save(ignore_permissions=True)
- sales_invoice.submit()
- frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
+ item = sales_invoice.append('items', {})
+ item = get_appointment_item(appointment_doc, item)
+
+ # Add payments if payment details are supplied else proceed to create invoice as Unpaid
+ if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
+ sales_invoice.is_pos = 1
+ payment = sales_invoice.append('payments', {})
+ payment.mode_of_payment = appointment_doc.mode_of_payment
+ payment.amount = appointment_doc.paid_amount
+
+ sales_invoice.set_missing_values(for_validate=True)
+ sales_invoice.flags.ignore_mandatory = True
+ sales_invoice.save(ignore_permissions=True)
+ sales_invoice.submit()
+ frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
+ frappe.db.set_value('Patient Appointment', appointment_doc.name, {
+ 'invoiced': 1,
+ 'ref_sales_invoice': sales_invoice.name
+ })
def check_is_new_patient(patient, name=None):
@@ -187,13 +201,14 @@
def get_appointment_item(appointment_doc, item):
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc)
- item.item_code = service_item
+ details = get_service_item_and_practitioner_charge(appointment_doc)
+ charge = appointment_doc.paid_amount or details.get('practitioner_charge')
+ item.item_code = details.get('service_item')
item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company)
item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
- item.rate = practitioner_charge
- item.amount = practitioner_charge
+ item.rate = charge
+ item.amount = charge
item.qty = 1
item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index f7ec6f5..2bb8a53 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -32,7 +32,8 @@
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
- self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1)
+ appointment.reload()
+ self.assertEqual(appointment.invoiced, 1)
encounter = make_encounter(appointment.name)
self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company)
@@ -41,7 +42,7 @@
# invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
- def test_invoicing(self):
+ def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
@@ -57,6 +58,50 @@
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+ def test_auto_invoicing_based_on_department(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ appointment_type = create_appointment_type()
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, 'HLC-SI-001')
+ self.assertEqual(appointment.paid_amount, 200)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+ self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+
+ def test_auto_invoicing_according_to_appointment_type_charge(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+
+ item = create_healthcare_service_items()
+ items = [{
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 300
+ }]
+ appointment_type = create_appointment_type(args={
+ 'name': 'Generic Appointment Type charge',
+ 'items': items
+ })
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name)
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, item)
+ self.assertEqual(appointment.paid_amount, 300)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@@ -178,14 +223,15 @@
encounter.submit()
return encounter
-def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
+def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
+ service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient
appointment.practitioner = practitioner
- appointment.department = '_Test Medical Department'
+ appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
@@ -193,7 +239,8 @@
appointment.service_unit = service_unit
if invoice:
appointment.mode_of_payment = 'Cash'
- appointment.paid_amount = 500
+ if appointment_type:
+ appointment.appointment_type = appointment_type
if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
@@ -223,4 +270,29 @@
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
- return template
\ No newline at end of file
+ return template
+
+def create_appointment_type(args=None):
+ if not args:
+ args = frappe.local.form_dict
+
+ name = args.get('name') or 'Test Appointment Type wise Charge'
+
+ if frappe.db.exists('Appointment Type', name):
+ return frappe.get_doc('Appointment Type', name)
+
+ else:
+ item = create_healthcare_service_items()
+ items = [{
+ 'medical_department': '_Test Medical Department',
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 200
+ }]
+ return frappe.get_doc({
+ 'doctype': 'Appointment Type',
+ 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge',
+ 'default_duration': args.get('default_duration') or 20,
+ 'color': args.get('color') or '#7575ff',
+ 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
+ 'items': args.get('items') or items
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index d4027df..d3d22c8 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -5,9 +5,11 @@
from __future__ import unicode_literals
import math
import frappe
+import json
from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded
+from six import string_types
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
@@ -64,7 +66,9 @@
income_account = None
service_item = None
if appointment.practitioner:
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment)
+ details = get_service_item_and_practitioner_charge(appointment)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({
'reference_type': 'Patient Appointment',
@@ -97,7 +101,9 @@
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
+ details = get_service_item_and_practitioner_charge(encounter)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({
@@ -173,7 +179,7 @@
if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced:
- service_item = get_healthcare_service_item('clinical_procedure_consumable_item')
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ')
msg += '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>'''
@@ -304,24 +310,50 @@
return therapy_sessions_to_invoice
-
+@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ service_item = None
+ practitioner_charge = None
+ department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department
+
is_inpatient = doc.inpatient_record
- if is_inpatient:
- service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item')
+
+ if doc.get('appointment_type'):
+ service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient)
+
+ if not service_item and not practitioner_charge:
+ service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient)
if not service_item:
- service_item = get_healthcare_service_item('inpatient_visit_charge_item')
- else:
- service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item')
- if not service_item:
- service_item = get_healthcare_service_item('op_consulting_charge_item')
+ service_item = get_healthcare_service_item(is_inpatient)
+
if not service_item:
throw_config_service_item(is_inpatient)
- practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner)
+ return {'service_item': service_item, 'practitioner_charge': practitioner_charge}
+
+
+def get_appointment_type_service_item(appointment_type, department, is_inpatient):
+ from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department
+
+ item_list = get_service_item_based_on_department(appointment_type, department)
+ service_item = None
+ practitioner_charge = None
+
+ if item_list:
+ if is_inpatient:
+ service_item = item_list.get('inpatient_visit_charge_item')
+ practitioner_charge = item_list.get('inpatient_visit_charge')
+ else:
+ service_item = item_list.get('op_consulting_charge_item')
+ practitioner_charge = item_list.get('op_consulting_charge')
+
return service_item, practitioner_charge
@@ -345,12 +377,27 @@
frappe.throw(msg, title=_('Missing Configuration'))
-def get_practitioner_service_item(practitioner, service_item_field):
- return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field)
+def get_practitioner_service_item(practitioner, is_inpatient):
+ service_item = None
+ practitioner_charge = None
+
+ if is_inpatient:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge'])
+ else:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge'])
+
+ return service_item, practitioner_charge
-def get_healthcare_service_item(service_item_field):
- return frappe.db.get_single_value('Healthcare Settings', service_item_field)
+def get_healthcare_service_item(is_inpatient):
+ service_item = None
+
+ if is_inpatient:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item')
+ else:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item')
+
+ return service_item
def get_practitioner_charge(practitioner, is_inpatient):
@@ -381,7 +428,8 @@
invoiced = True
if item.reference_dt == 'Clinical Procedure':
- if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
+ if service_item == item.item_code:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced)
@@ -403,7 +451,8 @@
def validate_invoiced_on_submit(item):
- if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ if item.reference_dt == 'Clinical Procedure' and \
+ frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 06a8e19..00e8c54 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -94,11 +94,11 @@
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1)
- bin1_on_submit = get_bin(item, warehouse)
+ reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production)
# reserved qty for production is updated
- self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2,
- cint(bin1_on_submit.reserved_qty_for_production))
+ self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
+
test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100)
@@ -109,9 +109,9 @@
s.submit()
bin1_at_completion = get_bin(item, warehouse)
-
+
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production),
- cint(bin1_on_submit.reserved_qty_for_production) - 1)
+ reserved_qty_on_submission - 1)
def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 2d3bc57..60aff02 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1103,10 +1103,10 @@
self.calculate_total_for_salary_slip_based_on_timesheet()
else:
self.total_deduction = 0.0
- if self.earnings:
+ if hasattr(self, "earnings"):
for earning in self.earnings:
self.gross_pay += flt(earning.amount, earning.precision("amount"))
- if self.deductions:
+ if hasattr(self, "deductions"):
for deduction in self.deductions:
self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 557c715..3c0652eb 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -34,9 +34,8 @@
"companies",
"currency_and_price_list",
"default_currency",
- "default_price_list",
"column_break_14",
- "language",
+ "default_price_list",
"address_contacts",
"address_html",
"website",
@@ -59,6 +58,7 @@
"column_break_45",
"market_segment",
"industry",
+ "language",
"is_frozen",
"column_break_38",
"loyalty_program",
@@ -485,7 +485,7 @@
"idx": 363,
"image_field": "image",
"links": [],
- "modified": "2020-03-17 11:03:42.706907",
+ "modified": "2021-01-06 19:35:25.418017",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 1516dd6..e561291 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -180,6 +180,7 @@
update_coupon_code_count(self.coupon_code,'used')
def on_cancel(self):
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index cbfab82..52a0174 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -17,6 +17,18 @@
from erpnext.stock.doctype.item.test_item import make_item
class TestSalesOrder(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order"))
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ # reset config to previous state
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
+
def tearDown(self):
frappe.set_user("Administrator")
@@ -1049,6 +1061,38 @@
self.assertRaises(frappe.LinkExistsError, so_doc.cancel)
+ def test_cancel_sales_order_after_cancel_payment_entry(self):
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+ # make a sales order
+ so = make_sales_order()
+
+ # disable unlinking of payment entry
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", 0)
+
+ # create a payment entry against sales order
+ pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC")
+ pe.reference_no = "1"
+ pe.reference_date = nowdate()
+ pe.paid_from_account_currency = so.currency
+ pe.paid_to_account_currency = so.currency
+ pe.source_exchange_rate = 1
+ pe.target_exchange_rate = 1
+ pe.paid_amount = so.grand_total
+ pe.save(ignore_permissions=True)
+ pe.submit()
+
+ # Cancel payment entry
+ po_doc = frappe.get_doc("Payment Entry", pe.name)
+ po_doc.cancel()
+
+ # Cancel sales order
+ try:
+ so_doc = frappe.get_doc('Sales Order', so.name)
+ so_doc.cancel()
+ except Exception:
+ self.fail("Can not cancel sales order with linked cancelled payment entry")
+
def test_request_for_raw_materials(self):
item = make_item("_Test Finished Item", {"is_stock_item": 1,
"maintain_stock": 1,
@@ -1207,4 +1251,4 @@
))
workflow.insert(ignore_permissions=True)
- return workflow
\ No newline at end of file
+ return workflow
diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py
index b3ae804..9e240cc 100644
--- a/erpnext/stock/__init__.py
+++ b/erpnext/stock/__init__.py
@@ -70,4 +70,4 @@
return account
def get_company_default_inventory_account(company):
- return frappe.get_cached_value('Company', company, 'default_inventory_account')
+ return frappe.get_cached_value('Company', company, 'default_inventory_account')
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 1088b41..0514bd2 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -16,8 +16,9 @@
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
+
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
@@ -34,11 +35,13 @@
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
- "sle_id": args.name
+ "sle_id": args.name,
+ "creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
- # Validate negative qty in future transactions
- validate_negative_qty_in_future_sle(args)
+ # update qty in future ale and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
def update_qty(self, args):
# update the stock values (for current quantities)
@@ -51,7 +54,7 @@
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
+
self.set_projected_qty()
self.db_update()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 559f8be..d39b229 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -489,7 +489,10 @@
def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status
- dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
+ make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
+
+ dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
dn.submit()
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index ca58ab2..7741ee7 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -94,10 +94,15 @@
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, "stock_value")
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
+
+ if existing_bin_qty < 0:
+ make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
pr = make_purchase_receipt()
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index f22c601..8436acb 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -46,6 +46,9 @@
def repost(doc):
try:
+ if not frappe.db.exists("Repost Item Valuation", doc.name):
+ return
+
doc.set_status('In Progress')
frappe.db.commit()
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 36d09ef..b0e7440 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -38,6 +38,7 @@
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
+
def on_submit(self):
self.check_stock_frozen_date()
self.actual_amt_check()
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 21860b6..e4f5725 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -23,6 +23,7 @@
cancel = sl_entries[0].get("is_cancelled")
if cancel:
+ validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
@@ -45,6 +46,20 @@
args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+def validate_cancellation(args):
+ if args[0].get("is_cancelled"):
+ repost_entry = frappe.db.get_value("Repost Item Valuation", {
+ 'voucher_type': args[0].voucher_type,
+ 'voucher_no': args[0].voucher_no,
+ 'docstatus': 1
+ }, ['name', 'status'], as_dict=1)
+
+ if repost_entry:
+ if repost_entry.status == 'In Progress':
+ frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
+ if repost_entry.status == 'Queued':
+ frappe.delete_doc("Repost Item Valuation", repost_entry.name)
+
def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
@@ -74,7 +89,8 @@
"item_code": args[i].item_code,
"warehouse": args[i].warehouse,
"posting_date": args[i].posting_date,
- "posting_time": args[i].posting_time
+ "posting_time": args[i].posting_time,
+ "creation": args[i].get("creation")
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
for item_wh, new_sle in iteritems(obj.new_items):
@@ -86,7 +102,7 @@
def get_args_for_voucher(voucher_type, voucher_no):
return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
- fields=["item_code", "warehouse", "posting_date", "posting_time"],
+ fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse"
)
@@ -117,7 +133,7 @@
self.item_code = args.get("item_code")
if self.args.sle_id:
self.args['name'] = self.args.sle_id
-
+
self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
self.get_precision()
self.valuation_method = get_valuation_method(self.item_code)
@@ -155,7 +171,7 @@
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
- previous_sle = self.get_sle_before_datetime(args)
+ previous_sle = self.get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -167,9 +183,35 @@
"stock_value_difference": 0.0
})
+ def get_previous_sle_of_current_voucher(self, args):
+ """get stock ledger entries filtered by specific posting datetime conditions"""
+
+ args['time_format'] = '%H:%i:%s'
+ if not args.get("posting_date"):
+ args["posting_date"] = "1900-01-01"
+ if not args.get("posting_time"):
+ args["posting_time"] = "00:00"
+
+ sle = frappe.db.sql("""
+ select *, timestamp(posting_date, posting_time) as "timestamp"
+ from `tabStock Ledger Entry`
+ where item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and is_cancelled = 0
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ order by timestamp(posting_date, posting_time) desc, creation desc
+ limit 1""", args, as_dict=1)
+
+ return sle[0] if sle else frappe._dict()
+
+
def build(self):
+ from erpnext.controllers.stock_controller import check_if_future_sle_exists
+
if self.args.get("sle_id"):
- self.process_sle_against_current_voucher()
+ self.process_sle_against_current_timestamp()
+ if not check_if_future_sle_exists(self.args):
+ self.update_bin()
else:
entries_to_fix = self.get_future_entries_to_fix()
@@ -182,13 +224,13 @@
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
+
+ self.update_bin()
if self.exceptions:
self.raise_exceptions()
- self.update_bin()
-
- def process_sle_against_current_voucher(self):
+ def process_sle_against_current_timestamp(self):
sl_entries = self.get_sle_against_current_voucher()
for sle in sl_entries:
self.process_sle(sle)
@@ -204,8 +246,8 @@
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
- and voucher_type = %(voucher_type)s
- and voucher_no = %(voucher_no)s
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+
order by
creation ASC
for update
@@ -232,7 +274,6 @@
return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
-
self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle \
@@ -639,7 +680,6 @@
# update bin for each warehouse
for warehouse, data in iteritems(self.data):
bin_doc = get_bin(self.item_code, warehouse)
-
bin_doc.update({
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
@@ -765,6 +805,25 @@
return valuation_rate
+def update_qty_in_future_sle(args, allow_negative_stock=None):
+ frappe.db.sql("""
+ update `tabStock Ledger Entry`
+ set qty_after_transaction = qty_after_transaction + {qty}
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ """.format(qty=args.actual_qty), args)
+
+ validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
@@ -793,7 +852,7 @@
and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and is_cancelled = 0
- and qty_after_transaction + {0} < 0
+ and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
limit 1
- """.format(args.actual_qty), args, as_dict=1)
+ """, args, as_dict=1)
\ No newline at end of file