diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 94573f9df3..76e00923c4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -179,10 +179,18 @@ class POSInvoice(SalesInvoice): 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 @@ class POSInvoice(SalesInvoice): _("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 @@ class POSInvoice(SalesInvoice): 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 57a23af8af..15875afe87 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 @@ class TestPOSInvoice(unittest.TestCase): 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 2b6e7de118..8b71eb02fd 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 c88d67989b..40f77b4088 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 @@ 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: @@ -58,7 +58,7 @@ class POSInvoiceMergeLog(Document): 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 @@ class POSInvoiceMergeLog(Document): 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 @@ class POSInvoiceMergeLog(Document): 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 @@ class POSInvoiceMergeLog(Document): 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 @@ class POSInvoiceMergeLog(Document): 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 @@ def get_all_unconsolidated_invoices(): } 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 @@ def get_invoice_customer_map(pos_invoices): 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 d0116890b6..76f3c50578 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 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i 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 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com 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 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g 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 40362b1d40..4cc5753cbd 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 0e1829a767..de61b35316 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -204,8 +204,6 @@ def get_already_returned_items(doc): 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 @@ def make_return_doc(doctype, source_name, target_doc=None): 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 2df1793fdb..1b33fd73ac 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 eee13f7e79..2e09a76c0f 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 15916a5134..861675acea 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 58753bb4f0..3872318287 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 1dacffab35..67a24f31e0 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 0000000000..e69de29bb2 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 0000000000..5ff68cd682 --- /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 0000000000..b2e0e82bad --- /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 cb747f95ef..8162f03f6d 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 3d5073b13e..0354733dfb 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', { }); 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 @@ frappe.ui.form.on('Patient Appointment', { 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 @@ frappe.ui.form.on('Patient Appointment', { } }, + 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 @@ frappe.ui.form.on('Patient Appointment', { // 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 @@ let update_status = function(frm, status){ ); }; -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 35600e4809..83c92af36a 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 f2b94b8e9c..1f76cd624c 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -26,6 +26,7 @@ class PatientAppointment(Document): 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 @@ class PatientAppointment(Document): 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 @@ def invoice_appointment(appointment_doc): 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 check_is_new_patient(patient, name=None): 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 f7ec6f58fc..2bb8a53c45 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 @@ class TestPatientAppointment(unittest.TestCase): 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 @@ class TestPatientAppointment(unittest.TestCase): # 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 @@ class TestPatientAppointment(unittest.TestCase): 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 @@ def create_encounter(appointment): 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 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce 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 @@ def create_clinical_procedure_template(): 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 d4027dff4e..d3d22c80b6 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 @@ def get_appointments_to_invoice(patient, company): 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 @@ def get_encounters_to_invoice(patient, company): 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 @@ def get_clinical_procedures_to_invoice(patient, company): 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 += '''Healthcare Settings''' @@ -304,24 +310,50 @@ def get_therapy_sessions_to_invoice(patient, company): 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 @@ def throw_config_practitioner_charge(is_inpatient, practitioner): 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 @@ def set_invoiced(item, method, ref_invoice=None): 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 set_invoiced(item, method, ref_invoice=None): 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 06a8e1987d..00e8c5418a 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 @@ class TestWorkOrder(unittest.TestCase): 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 @@ class TestWorkOrder(unittest.TestCase): 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 2d3bc57900..60aff02b38 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1103,10 +1103,10 @@ class SalarySlip(TransactionBase): 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 557c7151d9..3c0652eb67 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 1516dd6d95..e56129170c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -180,6 +180,7 @@ class SalesOrder(SellingController): 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 cbfab8204f..52a0174798 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.selling.doctype.product_bundle.test_product_bundle import make_prod 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 @@ class TestSalesOrder(unittest.TestCase): 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 @@ def make_sales_order_workflow(): )) 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 b3ae804b1c..9e240cc2b3 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -70,4 +70,4 @@ def get_warehouse_account(warehouse, warehouse_account=None): 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 1088b4127d..0514bd2394 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -16,8 +16,9 @@ class Bin(Document): 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 @@ class Bin(Document): "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 @@ class Bin(Document): 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 559f8be0de..d39b22965e 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 @@ class TestDeliveryNote(unittest.TestCase): 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 ca58ab2823..7741ee7f60 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 @@ class TestPurchaseReceipt(unittest.TestCase): 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 f22c6018d4..8436acbed2 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 @@ class RepostItemValuation(Document): 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 36d09efd1a..b0e7440e6c 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 @@ class StockLedgerEntry(Document): 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 21860b6863..e4f5725c68 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -23,6 +23,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc 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 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc 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 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat "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 repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat 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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): """ 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 @@ class update_entries_after(object): "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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): 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 @@ class update_entries_after(object): # 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 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, 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 @@ def get_future_sle_with_negative_qty(args): 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