diff --git a/erpnext/accounts/accounts b/erpnext/accounts/accounts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index cdd166134f9..cdd375802db 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -1,127 +1,264 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from erpnext import get_default_company import frappe import json +from frappe.utils import nowdate, add_months, get_date_str +from frappe import _ +from erpnext.accounts.utils import get_fiscal_year, get_account_name +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None def get_data(): - data = frappe._dict({ - "dashboards": [], - "charts": [] + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards() }) - company = get_company_for_dashboards() - if company: - company_doc = frappe.get_doc("Company", company) - data.dashboards = get_dashboards() - data.charts = get_charts(company_doc) - return data def get_dashboards(): return [{ "name": "Accounts", "dashboard_name": "Accounts", + "doctype": "Dashboard", "charts": [ - { "chart": "Outgoing Bills (Sales Invoice)" }, - { "chart": "Incoming Bills (Purchase Invoice)" }, - { "chart": "Bank Balance" }, - { "chart": "Income" }, - { "chart": "Expenses" } + { "chart": "Profit and Loss" , "width": "Full"}, + { "chart": "Incoming Bills (Purchase Invoice)", "width": "Half"}, + { "chart": "Outgoing Bills (Sales Invoice)", "width": "Half"}, + { "chart": "Accounts Receivable Ageing", "width": "Half"}, + { "chart": "Accounts Payable Ageing", "width": "Half"}, + { "chart": "Budget Variance", "width": "Full"}, + { "chart": "Bank Balance", "width": "Full"} + ], + "cards": [ + {"card": "Total Outgoing Bills"}, + {"card": "Total Incoming Bills"}, + {"card": "Total Incoming Payment"}, + {"card": "Total Outgoing Payment"} ] }] -def get_charts(company): - income_account = company.default_income_account or get_account("Income Account", company.name) - expense_account = company.default_expense_account or get_account("Expense Account", company.name) - bank_account = company.default_bank_account or get_account("Bank", company.name) +def get_charts(): + company = frappe.get_doc("Company", get_company_for_dashboards()) + bank_account = company.default_bank_account or get_account_name("Bank", company=company.name) + fiscal_year = get_fiscal_year(date=nowdate()) + default_cost_center = company.cost_center return [ { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Income", - "chart_name": "Income", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": income_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, + "doctype": "Dashboard Charts", + "name": "Profit and Loss", "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Expenses", - "chart_name": "Expenses", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": expense_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Bank Balance", - "chart_name": "Bank Balance", - "timespan": "Last Year", - "color": "#ffb868", - "filters_json": json.dumps({"company": company.name, "account": bank_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" + "report_name": "Profit and Loss Statement", + "filters_json": json.dumps({ + "company": company.name, + "filter_based_on": "Date Range", + "period_start_date": get_date_str(fiscal_year[1]), + "period_end_date": get_date_str(fiscal_year[2]), + "periodicity": "Monthly", + "include_default_book_entries": 1 + }), + "type": "Bar", + 'timeseries': 0, + "chart_type": "Report", + "chart_name": _("Profit and Loss"), + "is_custom": 1, + "is_public": 1 }, { "doctype": "Dashboard Chart", "time_interval": "Monthly", "name": "Incoming Bills (Purchase Invoice)", - "chart_name": "Incoming Bills (Purchase Invoice)", + "chart_name": _("Incoming Bills (Purchase Invoice)"), "timespan": "Last Year", "color": "#a83333", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), + "value_based_on": "base_net_total", + "filters_json": json.dumps({"docstatus": 1}), "chart_type": "Sum", "timeseries": 1, "based_on": "posting_date", "owner": "Administrator", "document_type": "Purchase Invoice", - "type": "Bar" + "type": "Bar", + "width": "Half", + "is_public": 1 }, { "doctype": "Dashboard Chart", - "time_interval": "Monthly", "name": "Outgoing Bills (Sales Invoice)", - "chart_name": "Outgoing Bills (Sales Invoice)", + "time_interval": "Monthly", + "chart_name": _("Outgoing Bills (Sales Invoice)"), "timespan": "Last Year", "color": "#7b933d", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), + "value_based_on": "base_net_total", + "filters_json": json.dumps({"docstatus": 1}), "chart_type": "Sum", "timeseries": 1, "based_on": "posting_date", "owner": "Administrator", "document_type": "Sales Invoice", - "type": "Bar" - } + "type": "Bar", + "width": "Half", + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Accounts Receivable Ageing", + "owner": "Administrator", + "report_name": "Accounts Receivable", + "filters_json": json.dumps({ + "company": company.name, + "report_date": nowdate(), + "ageing_based_on": "Due Date", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120 + }), + "type": "Donut", + 'timeseries': 0, + "chart_type": "Report", + "chart_name": _("Accounts Receivable Ageing"), + "is_custom": 1, + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Accounts Payable Ageing", + "owner": "Administrator", + "report_name": "Accounts Payable", + "filters_json": json.dumps({ + "company": company.name, + "report_date": nowdate(), + "ageing_based_on": "Due Date", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120 + }), + "type": "Donut", + 'timeseries': 0, + "chart_type": "Report", + "chart_name": _("Accounts Payable Ageing"), + "is_custom": 1, + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Budget Variance", + "owner": "Administrator", + "report_name": "Budget Variance Report", + "filters_json": json.dumps({ + "company": company.name, + "from_fiscal_year": fiscal_year[0], + "to_fiscal_year": fiscal_year[0], + "period": "Monthly", + "budget_against": "Cost Center" + }), + "type": "Bar", + "timeseries": 0, + "chart_type": "Report", + "chart_name": _("Budget Variance"), + "is_custom": 1, + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Bank Balance", + "time_interval": "Quarterly", + "chart_name": "Bank Balance", + "timespan": "Last Year", + "filters_json": json.dumps({ + "company": company.name, + "account": bank_account + }), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line", + "width": "Half", + "is_public": 1 + }, ] -def get_account(account_type, company): - accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) - if accounts: - return accounts[0].name - -def get_company_for_dashboards(): - company = get_default_company() - if not company: - company_list = frappe.get_list("Company") - if company_list: - company = company_list[0].name - return company +def get_number_cards(): + fiscal_year = get_fiscal_year(date=nowdate()) + year_start_date = get_date_str(fiscal_year[1]) + year_end_date = get_date_str(fiscal_year[2]) + return [ + { + "doctype": "Number Card", + "document_type": "Payment Entry", + "name": "Total Incoming Payment", + "filters_json": json.dumps([ + ['Payment Entry', 'docstatus', '=', 1], + ['Payment Entry', 'posting_date', 'between', [year_start_date, year_end_date]], + ['Payment Entry', 'payment_type', '=', 'Receive'] + ]), + "label": _("Total Incoming Payment"), + "function": "Sum", + "aggregate_function_based_on": "base_received_amount", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Payment Entry", + "name": "Total Outgoing Payment", + "filters_json": json.dumps([ + ['Payment Entry', 'docstatus', '=', 1], + ['Payment Entry', 'posting_date', 'between', [year_start_date, year_end_date]], + ['Payment Entry', 'payment_type', '=', 'Pay'] + ]), + "label": _("Total Outgoing Payment"), + "function": "Sum", + "aggregate_function_based_on": "base_paid_amount", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Sales Invoice", + "name": "Total Outgoing Bills", + "filters_json": json.dumps([ + ['Sales Invoice', 'docstatus', '=', 1], + ['Sales Invoice', 'posting_date', 'between', [year_start_date, year_end_date]] + ]), + "label": _("Total Outgoing Bills"), + "function": "Sum", + "aggregate_function_based_on": "base_net_total", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Purchase Invoice", + "name": "Total Incoming Bills", + "filters_json": json.dumps([ + ['Purchase Invoice', 'docstatus', '=', 1], + ['Purchase Invoice', 'posting_date', 'between', [year_start_date, year_end_date]] + ]), + "label": _("Total Incoming Bills"), + "function": "Sum", + "aggregate_function_based_on": "base_net_total", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + } + ] diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 0d6aca65b16..a783b1d0dbb 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -45,11 +45,6 @@ "label": "Bank Statement", "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, - { - "hidden": 0, - "links": "[\n {\n \"description\": \"Match non-linked Invoices and Payments.\",\n \"label\": \"Match Payments with Invoices\",\n \"name\": \"Payment Reconciliation\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update bank payment dates with journals.\",\n \"label\": \"Update Bank Clearance Dates\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Invoice Discounting\",\n \"name\": \"Invoice Discounting\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Clearance Summary\",\n \"name\": \"Bank Clearance Summary\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Guarantee\",\n \"name\": \"Bank Guarantee\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup cheque dimensions for printing\",\n \"label\": \"Cheque Print Template\",\n \"name\": \"Cheque Print Template\",\n \"type\": \"doctype\"\n }\n]", - "title": "Banking and Payments" - }, { "hidden": 0, "label": "Subscription Management", @@ -89,8 +84,8 @@ "category": "Modules", "charts": [ { - "chart_name": "Bank Balance", - "label": "Bank Balance" + "chart_name": "Profit and Loss", + "label": "Profit and Loss" } ], "creation": "2020-03-02 15:41:59.515192", @@ -99,23 +94,38 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-04-29 12:17:34.844397", + "modified": "2020-05-18 17:27:26.882340", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", + "onboarding": "Accounts", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { - "label": "Account", + "label": "Chart Of Accounts", "link_to": "Account", "type": "DocType" }, + { + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "type": "DocType" + }, + { + "label": "Purchase Invoice", + "link_to": "Purchase Invoice", + "type": "DocType" + }, + { + "label": "Accounts Dashboard", + "link_to": "Accounts Dashboard", + "type": "Dashboard" + }, { "label": "Journal Entry", "link_to": "Journal Entry", @@ -136,11 +146,6 @@ "link_to": "General Ledger", "type": "Report" }, - { - "label": "Profit and Loss Statement", - "link_to": "Profit and Loss Statement", - "type": "Report" - }, { "label": "Trial Balance", "link_to": "Trial Balance", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 19f571fb306..4d9053a55b3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -99,7 +99,7 @@ class PricingRule(Document): self.same_item = 1 def validate_max_discount(self): - if self.rate_or_discount == "Discount Percentage" and self.items: + if self.rate_or_discount == "Discount Percentage" and self.get("items"): for d in self.items: max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount") if max_discount and flt(self.discount_percentage) > flt(max_discount): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index f248276e5bb..df0c3d2299b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -924,7 +924,7 @@ var get_healthcare_services_to_invoice = function(frm) { if(patient && patient!=selected_patient){ selected_patient = patient; var method = "erpnext.healthcare.utils.get_healthcare_services_to_invoice"; - var args = {patient: patient}; + var args = {patient: patient, company: frm.doc.company}; var columns = (["service", "reference_name", "reference_type"]); get_healthcare_items(frm, true, $results, $placeholder, method, args, columns); } @@ -1068,7 +1068,11 @@ var get_drugs_to_invoice = function(frm) { description:'Quantity will be calculated only for items which has "Nos" as UoM. You may change as required for each invoice item.', get_query: function(doc) { return { - filters: { patient: dialog.get_value("patient"), docstatus: 1 } + filters: { + patient: dialog.get_value("patient"), + company: frm.doc.company, + docstatus: 1 + } }; } }, diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index dd6b4fdc603..4d43919f62e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -58,7 +58,7 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "rate": tax_rate_detail.tax_withholding_rate, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name + "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category }) def get_tax_withholding_rates(tax_withholding, fiscal_year): diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json new file mode 100644 index 00000000000..12da4400284 --- /dev/null +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -0,0 +1,51 @@ +{ + "allow_roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + } + ], + "creation": "2020-05-13 19:03:32.564049", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-14 22:11:06.475938", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounts", + "owner": "Administrator", + "steps": [ + { + "step": "Chart Of Accounts" + }, + { + "step": "Setup Taxes" + }, + { + "step": "Create a Product" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Create Your First Purchase Invoice" + }, + { + "step": "Create a Customer" + }, + { + "step": "Create Your First Sales Invoice" + }, + { + "step": "Configure Account Settings" + } + ], + "subtitle": "Accounts, invoices and taxation.", + "success_message": "The Accounts module is now set up!", + "title": "Let's Setup Your Accounts and Taxes.", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json new file mode 100644 index 00000000000..cbd022bfdbe --- /dev/null +++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2020-05-13 19:58:20.928127", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:40:28.410447", + "modified_by": "Administrator", + "name": "Chart Of Accounts", + "owner": "Administrator", + "path": "Tree/Account", + "reference_document": "Account", + "show_full_form": 0, + "title": "Review Chart Of Accounts", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json b/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json new file mode 100644 index 00000000000..c8be357de0a --- /dev/null +++ b/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:53:00.876946", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-14 18:06:25.212923", + "modified_by": "Administrator", + "name": "Configure Account Settings", + "owner": "Administrator", + "reference_document": "Accounts Settings", + "show_full_form": 1, + "title": "Configure Account Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json new file mode 100644 index 00000000000..bb396d268ae --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:46:41.831517", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:46:41.831517", + "modified_by": "Administrator", + "name": "Create a Customer", + "owner": "Administrator", + "reference_document": "Customer", + "show_full_form": 0, + "title": "Create a Customer", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json b/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json new file mode 100644 index 00000000000..450bee1f40b --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:45:28.554605", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:45:28.554605", + "modified_by": "Administrator", + "name": "Create a Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Product", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 00000000000..7a64224bd43 --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 22:09:10.043554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 22:09:10.043554", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_full_form": 0, + "title": "Create a Supplier", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json new file mode 100644 index 00000000000..3a2b8d39253 --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 22:10:07.049704", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 22:10:07.049704", + "modified_by": "Administrator", + "name": "Create Your First Purchase Invoice", + "owner": "Administrator", + "reference_document": "Purchase Invoice", + "show_full_form": 1, + "title": "Create Your First Purchase Invoice ", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json new file mode 100644 index 00000000000..473de5079f5 --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:48:21.019019", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:48:21.019019", + "modified_by": "Administrator", + "name": "Create Your First Sales Invoice", + "owner": "Administrator", + "reference_document": "Sales Invoice", + "show_full_form": 1, + "title": "Create Your First Sales Invoice ", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json new file mode 100644 index 00000000000..8e0006762d1 --- /dev/null +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-13 19:29:43.844463", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:40:16.014413", + "modified_by": "Administrator", + "name": "Setup Taxes", + "owner": "Administrator", + "reference_document": "Sales Taxes and Charges Template", + "show_full_form": 1, + "title": "Lets create a Tax Template for Sales ", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e9c286fcf0d..a0a1b9783ac 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -546,7 +546,7 @@ class ReceivablePayableReport(object): self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = 30, 60, 90, 120 for i, days in enumerate([self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]): - if row.age <= days: + if cint(row.age) <= cint(days): index = i break diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 49c1d0f2ccd..05dc2826611 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -56,14 +56,26 @@ def execute(filters=None): row += totals data.append(row) - return columns, data + chart = get_chart_data(filters, columns, data) + return columns, data, None, chart def get_columns(filters): columns = [ - _(filters.get("budget_against")) - + ":Link/%s:150" % (filters.get("budget_against")), - _("Account") + ":Link/Account:150" + { + 'label': _(filters.get("budget_against")), + 'fieldtype': 'Link', + 'fieldname': 'budget_against', + 'options': filters.get('budget_against'), + 'width': 150 + }, + { + 'label': _('Account'), + 'fieldname': 'Account', + 'fieldtype': 'Link', + 'options': 'Account', + 'width': 150 + } ] group_months = False if filters["period"] == "Monthly" else True @@ -79,7 +91,12 @@ def get_columns(filters): _("Variance ") + " " + str(year[0]) ] for label in labels: - columns.append(label + ":Float:150") + columns.append({ + 'label': label, + 'fieldtype': 'Float', + 'fieldname': frappe.scrub(label), + 'width': 150 + }) else: for label in [ _("Budget") + " (%s)" + " " + str(year[0]), @@ -95,14 +112,23 @@ def get_columns(filters): else: label = label % formatdate(from_date, format_string="MMM") - columns.append(label + ":Float:150") + columns.append({ + 'label': label, + 'fieldtype': 'Float', + 'fieldname': frappe.scrub(label), + 'width': 150 + }) if filters["period"] != "Yearly": - return columns + [ - _("Total Budget") + ":Float:150", - _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150" - ] + for label in [_("Total Budget"), _("Total Actual"), _("Total Variance")]: + columns.append({ + 'label': label, + 'fieldtype': 'Float', + 'fieldname': frappe.scrub(label), + 'width': 150 + }) + + return columns else: return columns @@ -173,7 +199,7 @@ def get_dimension_target_details(filters): filters.budget_against, filters.company, ] - + filters.get("budget_against_filter") + + (filters.get("budget_against_filter") or []) ), as_dict=True) @@ -305,3 +331,49 @@ def get_fiscal_years(filters): }) return fiscal_year + +def get_chart_data(filters, columns, data): + + if not data: + return None + + labels = [] + + fiscal_year = get_fiscal_years(filters) + group_months = False if filters["period"] == "Monthly" else True + + for year in fiscal_year: + for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): + if filters['period'] == 'Yearly': + labels.append(year[0]) + else: + if group_months: + label = formatdate(from_date, format_string="MMM") + "-" \ + + formatdate(to_date, format_string="MMM") + labels.append(label) + else: + label = formatdate(from_date, format_string="MMM") + labels.append(label) + + no_of_columns = len(labels) + + budget_values, actual_values = [0] * no_of_columns, [0] * no_of_columns + for d in data: + values = d[2:] + index = 0 + + for i in range(no_of_columns): + budget_values[i] += values[index] + actual_values[i] += values[index+1] + index += 3 + + return { + 'data': { + 'labels': labels, + 'datasets': [ + {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, + {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} + ] + } + } + diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 2c18eeb83a1..012f1cad0ad 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -120,13 +120,7 @@ def get_data(): { "type": "report", "is_query_report": True, - "name": "Open Work Orders", - "doctype": "Work Order" - }, - { - "type": "report", - "is_query_report": True, - "name": "Work Orders in Progress", + "name": "Work Order Summary", "doctype": "Work Order" }, { @@ -135,12 +129,6 @@ def get_data(): "name": "Issued Items Against Work Order", "doctype": "Work Order" }, - { - "type": "report", - "is_query_report": True, - "name": "Completed Work Orders", - "doctype": "Work Order" - }, { "type": "report", "is_query_report": True, diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 5febfd6bf28..5fbc460a973 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,11 +8,14 @@ from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.utils import nowdate, getdate from collections import defaultdict from erpnext.stock.get_item_details import _get_item_tax_template +from frappe.utils import unique # searches for active employees def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] - return frappe.db.sql("""select name, employee_name from `tabEmployee` + fields = get_fields("Employee", ["name", "employee_name"]) + + return frappe.db.sql("""select {fields} from `tabEmployee` where status = 'Active' and docstatus < 2 and ({key} like %(txt)s @@ -24,6 +27,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, employee_name limit %(start)s, %(page_len)s""".format(**{ + 'fields': ", ".join(fields), 'key': searchfield, 'fcond': get_filters_cond(doctype, filters, conditions), 'mcond': get_match_cond(doctype) @@ -34,9 +38,12 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) - # searches for leads which are not converted + +# searches for leads which are not converted def lead_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, lead_name, company_name from `tabLead` + fields = get_fields("Lead", ["name", "lead_name", "company_name"]) + + return frappe.db.sql("""select {fields} from `tabLead` where docstatus < 2 and ifnull(status, '') != 'Converted' and ({key} like %(txt)s @@ -50,6 +57,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, lead_name limit %(start)s, %(page_len)s""".format(**{ + 'fields': ", ".join(fields), 'key': searchfield, 'mcond':get_match_cond(doctype) }), { @@ -59,6 +67,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + # searches for customer def customer_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] @@ -69,13 +78,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): else: fields = ["name", "customer_name", "customer_group", "territory"] - meta = frappe.get_meta("Customer") - searchfields = meta.get_search_fields() - searchfields = searchfields + [f for f in [searchfield or "name", "customer_name"] \ - if not f in searchfields] - fields = fields + [f for f in searchfields if not f in fields] + fields = get_fields("Customer", fields) - fields = ", ".join(fields) + searchfields = frappe.get_meta("Customer").get_search_fields() searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) return frappe.db.sql("""select {fields} from `tabCustomer` @@ -88,7 +93,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, customer_name limit %(start)s, %(page_len)s""".format(**{ - "fields": fields, + "fields": ", ".join(fields), "scond": searchfields, "mcond": get_match_cond(doctype), "fcond": get_filters_cond(doctype, filters, conditions).replace('%', '%%'), @@ -99,6 +104,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + # searches for supplier def supplier_query(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") @@ -106,7 +112,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "supplier_group"] else: fields = ["name", "supplier_name", "supplier_group"] - fields = ", ".join(fields) + + fields = get_fields("Supplier", fields) return frappe.db.sql("""select {field} from `tabSupplier` where docstatus < 2 @@ -119,7 +126,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, supplier_name limit %(start)s, %(page_len)s """.format(**{ - 'field': fields, + 'field': ', '.join(fields), 'key': searchfield, 'mcond':get_match_cond(doctype) }), { @@ -129,6 +136,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + def tax_account_query(doctype, txt, searchfield, start, page_len, filters): company_currency = erpnext.get_company_currency(filters.get('company')) @@ -153,6 +161,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): return tax_accounts + def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] @@ -221,10 +230,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals "page_len": page_len }, as_dict=as_dict) + def bom(doctype, txt, searchfield, start, page_len, filters): conditions = [] + fields = get_fields("BOM", ["name", "item"]) - return frappe.db.sql("""select tabBOM.name, tabBOM.item + return frappe.db.sql("""select {fields} from tabBOM where tabBOM.docstatus=1 and tabBOM.is_active=1 @@ -234,6 +245,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, name limit %(start)s, %(page_len)s """.format( + fields=", ".join(fields), fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), mcond=get_match_cond(doctype).replace('%', '%%'), key=searchfield), @@ -244,13 +256,16 @@ def bom(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len or 20 }) + def get_project_name(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters.get('customer'): cond = """(`tabProject`.customer = %s or ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer"))) - return frappe.db.sql("""select `tabProject`.name from `tabProject` + fields = get_fields("Project", ["name"]) + + return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") and {cond} `tabProject`.name like %(txt)s {match_cond} order by @@ -258,6 +273,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): idx desc, `tabProject`.name asc limit {start}, {page_len}""".format( + fields=", ".join(['`tabProject`.{0}'.format(f) for f in fields]), cond=cond, match_cond=get_match_cond(doctype), start=start, @@ -268,8 +284,10 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): + fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) + return frappe.db.sql(""" - select `tabDelivery Note`.name, `tabDelivery Note`.customer, `tabDelivery Note`.posting_date + select %(fields)s from `tabDelivery Note` where `tabDelivery Note`.`%(key)s` like %(txt)s and `tabDelivery Note`.docstatus = 1 @@ -284,6 +302,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, ) %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s """ % { + "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), "key": searchfield, "fcond": get_filters_cond(doctype, filters, []), "mcond": get_match_cond(doctype), @@ -349,6 +368,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): order by expiry_date, name desc limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args) + def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list = [] @@ -371,6 +391,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) + def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date from `tabBlanket Order` bo, `tabBlanket Order Item` boi @@ -385,6 +406,7 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): company = frappe.db.escape(filters.get("company")) )) + @frappe.whitelist() def get_income_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -490,6 +512,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters): item_filters = [ @@ -507,6 +530,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) ) return item_manufacturers + @frappe.whitelist() def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): query = """ @@ -520,6 +544,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): query = """ @@ -533,6 +558,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def get_tax_template(doctype, txt, searchfield, start, page_len, filters): @@ -556,3 +582,13 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): taxes = _get_item_tax_template(args, taxes, for_validate=True) return [(d,) for d in set(taxes)] + + +def get_fields(doctype, fields=[]): + meta = frappe.get_meta(doctype) + fields.extend(meta.get_search_fields()) + + if meta.title_field and not meta.title_field.strip() in fields: + fields.insert(1, meta.title_field.strip()) + + return unique(fields) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 20ab51d44bc..6fef0c46437 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_events_in_timeline": 1, "allow_import": 1, "autoname": "naming_series:", @@ -447,7 +448,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2020-04-08 22:26:11.687110", + "modified": "2020-05-11 20:27:45.868960", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -504,15 +505,6 @@ "read": 1, "report": 1, "role": "Sales User" - }, - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 } ], "search_fields": "lead_name,lead_owner,status", diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 618865200cf..44f87e0462e 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -49,12 +49,13 @@ def _order(*args, **kwargs): if event == "created": sys_lang = frappe.get_single("System Settings").language or 'en' raw_billing_data = order.get("billing") + raw_shipping_data = order.get("shipping") customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name") - link_customer_and_address(raw_billing_data, customer_name) + link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name) link_items(order.get("line_items"), woocommerce_settings, sys_lang) create_sales_order(order, woocommerce_settings, customer_name, sys_lang) -def link_customer_and_address(raw_billing_data, customer_name): +def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name): customer_woo_com_email = raw_billing_data.get("email") customer_exists = frappe.get_value("Customer", {"woocommerce_email": customer_woo_com_email}) if not customer_exists: @@ -68,38 +69,74 @@ def link_customer_and_address(raw_billing_data, customer_name): customer.customer_name = customer_name customer.woocommerce_email = customer_woo_com_email customer.flags.ignore_mandatory = True - customer.save() + customer.save() if customer_exists: frappe.rename_doc("Customer", old_name, customer_name) - address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email}) + billing_address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": "Billing"}) + shipping_address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": "Shipping"}) + rename_address(billing_address, customer) + rename_address(shipping_address, customer) else: - address = frappe.new_doc("Address") + create_address(raw_billing_data, customer, "Billing") + create_address(raw_shipping_data, customer, "Shipping") + create_contact(raw_billing_data, customer) - address.address_line1 = raw_billing_data.get("address_1", "Not Provided") - address.address_line2 = raw_billing_data.get("address_2", "Not Provided") - address.city = raw_billing_data.get("city", "Not Provided") - address.woocommerce_email = customer_woo_com_email - address.address_type = "Billing" - address.country = frappe.get_value("Country", {"code": raw_billing_data.get("country", "IN").lower()}) - address.state = raw_billing_data.get("state") - address.pincode = raw_billing_data.get("postcode") - address.phone = raw_billing_data.get("phone") - address.email_id = customer_woo_com_email +def create_contact(data, customer): + email = data.get("email", None) + phone = data.get("phone", None) + + if not email and not phone: + return + + contact = frappe.new_doc("Contact") + contact.first_name = data.get("first_name") + contact.last_name = data.get("last_name") + contact.is_primary_contact = 1 + contact.is_billing_contact = 1 + + if phone: + contact.add_phone(phone, is_primary_mobile_no=1, is_primary_phone=1) + + if email: + contact.add_email(email, is_primary=1) + + contact.append("links", { + "link_doctype": "Customer", + "link_name": customer.name + }) + + contact.flags.ignore_mandatory = True + contact.save() + +def create_address(raw_data, customer, address_type): + address = frappe.new_doc("Address") + + address.address_line1 = raw_data.get("address_1", "Not Provided") + address.address_line2 = raw_data.get("address_2", "Not Provided") + address.city = raw_data.get("city", "Not Provided") + address.woocommerce_email = customer.woocommerce_email + address.address_type = address_type + address.country = frappe.get_value("Country", {"code": raw_data.get("country", "IN").lower()}) + address.state = raw_data.get("state") + address.pincode = raw_data.get("postcode") + address.phone = raw_data.get("phone") + address.email_id = customer.woocommerce_email address.append("links", { "link_doctype": "Customer", - "link_name": customer.customer_name + "link_name": customer.name }) + address.flags.ignore_mandatory = True - address = address.save() + address.save() - if customer_exists: - old_address_title = address.name - new_address_title = customer.customer_name + "-billing" - address.address_title = customer.customer_name - address.save() +def rename_address(address, customer): + old_address_title = address.name + new_address_title = customer.name + "-" + address.address_type + address.address_title = customer.customer_name + address.save() - frappe.rename_doc("Address", old_address_title, new_address_title) + frappe.rename_doc("Address", old_address_title, new_address_title) def link_items(items_list, woocommerce_settings, sys_lang): for item_data in items_list: @@ -111,7 +148,7 @@ def link_items(items_list, woocommerce_settings, sys_lang): else: #Create Item item = frappe.new_doc("Item") - + item.item_name = item_data.get("name") item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id")) item.woocommerce_id = item_data.get("product_id") @@ -171,7 +208,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l add_tax_details(new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account) add_tax_details(new_sales_order, order.get("shipping_total"), "Shipping Total", woocommerce_settings.f_n_f_account) - + def add_tax_details(sales_order, price, desc, tax_account_head): sales_order.append("taxes", { "charge_type":"Actual", diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js index 87c22ccf6f3..eb7d4bdebad 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js @@ -43,7 +43,8 @@ frappe.ui.form.on('Clinical Procedure', { return { filters: { 'is_group': false, - 'allow_appointments': true + 'allow_appointments': true, + 'company': frm.doc.company } }; }); @@ -158,11 +159,13 @@ frappe.ui.form.on('Clinical Procedure', { age = __('{0} as on {1}', [age, data.message.age_as_on]); } } + frm.set_value('patient_name', data.message.patient_name); frm.set_value('patient_age', age); frm.set_value('patient_sex', data.message.sex); } }); } else { + frm.set_value('patient_name', ''); frm.set_value('patient_age', ''); frm.set_value('patient_sex', ''); } @@ -177,15 +180,35 @@ frappe.ui.form.on('Clinical Procedure', { name: frm.doc.appointment }, callback: function(data) { - frm.set_value('patient', data.message.patient); - frm.set_value('procedure_template', data.message.procedure_template); - frm.set_value('medical_department', data.message.department); - frm.set_value('start_date', data.message.appointment_date); - frm.set_value('start_time', data.message.appointment_time); - frm.set_value('notes', data.message.notes); - frm.set_value('service_unit', data.message.service_unit); + let values = { + 'patient':data.message.patient, + 'procedure_template': data.message.procedure_template, + 'medical_department': data.message.department, + 'practitioner': data.message.practitioner, + 'start_date': data.message.appointment_date, + 'start_time': data.message.appointment_time, + 'notes': data.message.notes, + 'service_unit': data.message.service_unit, + 'company': data.message.company + }; + frm.set_value(values); } }); + } else { + let values = { + 'patient': '', + 'patient_name': '', + 'patient_sex': '', + 'patient_age': '', + 'medical_department': '', + 'procedure_template': '', + 'start_date': '', + 'start_time': '', + 'notes': '', + 'service_unit': '', + 'inpatient_record': '' + }; + frm.set_value(values); } }, @@ -234,9 +257,11 @@ frappe.ui.form.on('Clinical Procedure', { name: frm.doc.practitioner }, callback: function (data) { - frappe.model.set_value(frm.doctype,frm.docname, 'medical_department',data.message.department); + frappe.model.set_value(frm.doctype,frm.docname, 'practitioner_name', data.message.practitioner_name); } }); + } else { + frappe.model.set_value(frm.doctype,frm.docname, 'practitioner_name', ''); } }, @@ -284,14 +309,6 @@ frappe.ui.form.on('Clinical Procedure', { }); -cur_frm.set_query('procedure_template', function(doc) { - return { - filters: { - 'medical_department': doc.medical_department - } - }; -}); - frappe.ui.form.on('Clinical Procedure Item', { qty: function(frm, cdt, cdn) { let d = locals[cdt][cdn]; diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json index 3c936bbf277..eaf8d80ba8c 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json @@ -7,28 +7,32 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "inpatient_record", "naming_series", - "procedure_template", + "title", "appointment", + "procedure_template", + "column_break_30", + "company", + "invoiced", + "section_break_6", "patient", + "patient_name", "patient_sex", "patient_age", - "prescription", - "medical_department", - "practitioner", + "inpatient_record", + "notes", "column_break_7", "status", + "practitioner", + "practitioner_name", + "medical_department", "service_unit", - "warehouse", "start_date", "start_time", "sample", - "invoiced", - "notes", - "company", "consumables_section", "consume_stock", + "warehouse", "items", "section_break_24", "invoice_separately_as_consumables", @@ -36,6 +40,9 @@ "consumable_total_amount", "column_break_27", "consumption_details", + "sb_refs", + "column_break_34", + "prescription", "amended_from" ], "fields": [ @@ -56,15 +63,15 @@ { "fieldname": "appointment", "fieldtype": "Link", - "in_list_view": 1, + "in_standard_filter": 1, "label": "Appointment", - "options": "Patient Appointment" + "options": "Patient Appointment", + "set_only_once": 1 }, { - "fetch_from": "inpatient_record.patient", "fieldname": "patient", "fieldtype": "Link", - "in_list_view": 1, + "in_standard_filter": 1, "label": "Patient", "options": "Patient", "reqd": 1 @@ -88,17 +95,20 @@ "fieldtype": "Link", "hidden": 1, "label": "Procedure Prescription", - "options": "Procedure Prescription" + "options": "Procedure Prescription", + "read_only": 1 }, { "fieldname": "medical_department", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Medical Department", "options": "Medical Department" }, { "fieldname": "practitioner", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner" }, @@ -208,6 +218,7 @@ "read_only": 1 }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, @@ -226,6 +237,8 @@ "read_only": 1 }, { + "collapsible": 1, + "collapsible_depends_on": "consume_stock", "fieldname": "consumables_section", "fieldtype": "Section Break", "label": "Consumables" @@ -237,11 +250,51 @@ { "fieldname": "section_break_24", "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_30", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "sb_refs", + "fieldtype": "Section Break" + }, + { + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 + }, + { + "fieldname": "practitioner_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Practitioner Name", + "read_only": 1 + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-03-02 11:44:27.970651", + "modified": "2020-04-27 21:36:23.796924", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedure", @@ -257,11 +310,27 @@ "report": 1, "role": "Nursing User", "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "submit": 1, "write": 1 } ], "restrict_to_domain": "Healthcare", "sort_field": "modified", "sort_order": "DESC", + "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index b7d7a62a950..e55a1433a51 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -16,6 +16,7 @@ from frappe.model.mapper import get_mapped_doc class ClinicalProcedure(Document): def validate(self): self.set_status() + self.set_title() if self.consume_stock: self.set_actual_qty() @@ -37,7 +38,7 @@ class ClinicalProcedure(Document): template = frappe.get_doc('Clinical Procedure Template', self.procedure_template) if template.sample: patient = frappe.get_doc('Patient', self.patient) - sample_collection = create_sample_doc(template, patient, None) + sample_collection = create_sample_doc(template, patient, None, self.company) frappe.db.set_value('Clinical Procedure', self.name, 'sample', sample_collection.name) self.reload() @@ -50,6 +51,9 @@ class ClinicalProcedure(Document): elif self.docstatus == 2: self.status = 'Cancelled' + def set_title(self): + self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100] + def complete_procedure(self): if self.consume_stock and self.items: stock_entry = make_stock_entry(self) diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json index fd5b6e12f66..cb747f95ef8 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_copy": 1, "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", @@ -51,17 +50,20 @@ "fieldname": "first_name", "fieldtype": "Data", "label": "First Name", + "no_copy": 1, "reqd": 1 }, { "fieldname": "middle_name", "fieldtype": "Data", - "label": "Middle Name (Optional)" + "label": "Middle Name (Optional)", + "no_copy": 1 }, { "fieldname": "last_name", "fieldtype": "Data", - "label": "Last Name" + "label": "Last Name", + "no_copy": 1 }, { "fieldname": "image", @@ -226,6 +228,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Full Name", + "no_copy": 1, "read_only": 1, "search_index": 1 }, @@ -233,6 +236,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", + "no_copy": 1, "options": "HLC-PRAC-.YYYY.-", "report_hide": 1, "set_only_once": 1 diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json index de086201795..2f0115c36a7 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json @@ -240,7 +240,7 @@ "label": "Patient Registration" }, { - "default": "Hello {{doc.patient}}, Thank you for registering with {{doc.company}}. Your ID is {{doc.id}} . Please note this ID for future reference. \nThank You, Get well soon!", + "default": "Hello {{doc.patient}}, Thank you for registering with {{doc.company}}. Your ID is {{doc.name}} . Please note this ID for future reference. \nThank You!", "depends_on": "send_registration_msg", "fieldname": "registration_msg", "fieldtype": "Small Text", @@ -254,7 +254,7 @@ "label": "Appointment Confirmation" }, { - "default": "Hello {{doc.patient}}, You have scheduled an appointment with {{doc.practitioner}} by {{doc.start_dt}} at {{doc.company}}.\nThank you, Good day!", + "default": "Hello {{doc.patient}}, You have scheduled an appointment with {{doc.practitioner}} on {{doc.appointment_datetime}} at {{doc.company}}.\nThank you, Good day!", "depends_on": "send_appointment_confirmation", "fieldname": "appointment_confirmation_msg", "fieldtype": "Small Text", @@ -276,7 +276,7 @@ "label": "Appointment Reminder" }, { - "default": "Hello {{doc.patient}}, You have an appointment with {{doc.practitioner}} by {{doc.appointment_time}} at {{doc.company}}.\nThank you, Good day!\n", + "default": "Hello {{doc.patient}}, You have an appointment with {{doc.practitioner}} by {{doc.appointment_datetime}} at {{doc.company}}.\nThank you, Good day!\n", "depends_on": "send_appointment_reminder", "fieldname": "appointment_reminder_msg", "fieldtype": "Small Text", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index 92c11fbb4da..c1b516d5363 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -1,980 +1,255 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2018-07-11 17:48:51.404139", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "naming_series:", + "creation": "2018-07-11 17:48:51.404139", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_1", + "naming_series", + "patient", + "patient_name", + "gender", + "blood_group", + "dob", + "mobile", + "email", + "phone", + "column_break_8", + "company", + "status", + "scheduled_date", + "admitted_datetime", + "expected_discharge", + "discharge_date", + "references", + "cb_admission", + "admission_practitioner", + "admission_encounter", + "cb_discharge", + "discharge_practitioner", + "discharge_encounter", + "sb_inpatient_occupancy", + "inpatient_occupancies", + "btn_transfer", + "sb_discharge_note", + "discharge_note" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_1", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series", - "length": 0, - "no_copy": 0, - "options": "HLC-INP-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 1, + "label": "Series", + "options": "HLC-INP-.YYYY.-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "patient", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Patient", - "length": 0, - "no_copy": 0, - "options": "Patient", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "patient", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Patient", + "options": "Patient", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "patient.patient_name", - "fieldname": "patient_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Patient Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.patient_name", + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "patient.sex", - "fieldname": "gender", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Gender", - "length": 0, - "no_copy": 0, - "options": "Gender", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.sex", + "fieldname": "gender", + "fieldtype": "Link", + "label": "Gender", + "options": "Gender", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "patient.blood_group", - "fieldname": "blood_group", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Blood Group", - "length": 0, - "no_copy": 0, - "options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.blood_group", + "fieldname": "blood_group", + "fieldtype": "Select", + "label": "Blood Group", + "options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dob", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date of birth", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.dob", + "fieldname": "dob", + "fieldtype": "Date", + "label": "Date of birth", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "patient.mobile", - "fieldname": "mobile", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mobile", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.mobile", + "fieldname": "mobile", + "fieldtype": "Data", + "label": "Mobile", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "patient.email", - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Email", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.email", + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "options": "Email", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "patient.phone", - "fieldname": "phone", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Phone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "patient.phone", + "fieldname": "phone", + "fieldtype": "Data", + "label": "Phone", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Admission Scheduled\nAdmitted\nDischarge Scheduled\nDischarged", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Admission Scheduled\nAdmitted\nDischarge Scheduled\nDischarged", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "scheduled_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Admission Schedule Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "scheduled_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Admission Schedule Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "admitted_datetime", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Admitted Datetime", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "admitted_datetime", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Admitted Datetime" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expected_discharge", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Expected Discharge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "expected_discharge", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Expected Discharge" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "discharge_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Discharge Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "discharge_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Discharge Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "references", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "References", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "references", + "fieldtype": "Section Break", + "label": "References" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_admission", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Admission", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "cb_admission", + "fieldtype": "Column Break", + "label": "Admission" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "admission_practitioner", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Healthcare Practitioner", - "length": 0, - "no_copy": 0, - "options": "Healthcare Practitioner", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "admission_practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner", + "options": "Healthcare Practitioner", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "admission_encounter", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Patient Encounter", - "length": 0, - "no_copy": 0, - "options": "Patient Encounter", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "admission_encounter", + "fieldtype": "Link", + "label": "Patient Encounter", + "options": "Patient Encounter", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_discharge", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Discharge", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "cb_discharge", + "fieldtype": "Column Break", + "label": "Discharge" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "discharge_practitioner", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Healthcare Practitioner", - "length": 0, - "no_copy": 0, - "options": "Healthcare Practitioner", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "discharge_practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner", + "options": "Healthcare Practitioner", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "discharge_encounter", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Patient Encounter", - "length": 0, - "no_copy": 0, - "options": "Patient Encounter", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "discharge_encounter", + "fieldtype": "Link", + "label": "Patient Encounter", + "options": "Patient Encounter", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb_inpatient_occupancy", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Inpatient Occupancy", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "sb_inpatient_occupancy", + "fieldtype": "Section Break", + "label": "Inpatient Occupancy" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "inpatient_occupancies", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "options": "Inpatient Occupancy", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "inpatient_occupancies", + "fieldtype": "Table", + "options": "Inpatient Occupancy", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "btn_transfer", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Transfer", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "btn_transfer", + "fieldtype": "Button", + "label": "Transfer" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.status != \"Admission Scheduled\"", - "fieldname": "sb_discharge_note", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Discharge Note", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.status != \"Admission Scheduled\"", + "fieldname": "sb_discharge_note", + "fieldtype": "Section Break", + "label": "Discharge Note" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "discharge_note", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "discharge_note", + "fieldtype": "Text Editor" + }, + { + "fetch_from": "admission_encounter.company", + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 14:44:43.168245", - "modified_by": "Administrator", - "module": "Healthcare", - "name": "Inpatient Record", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-04-07 13:13:39.351977", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Inpatient Record", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Healthcare Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Healthcare", - "search_fields": "patient", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "patient", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + ], + "restrict_to_domain": "Healthcare", + "search_fields": "patient", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "patient", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index e15324c55bf..4c2d3f692a2 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -8,7 +8,6 @@ import unittest from frappe.utils import now_datetime, today from frappe.utils.make_random import get_random from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient class TestInpatientRecord(unittest.TestCase): def test_admit_and_discharge(self): @@ -112,3 +111,13 @@ def get_service_unit_type(): service_unit_type.save(ignore_permissions = True) return service_unit_type.name return service_unit_type + +def create_patient(): + patient = frappe.db.exists('Patient', '_Test IPD Patient') + if not patient: + patient = frappe.new_doc('Patient') + patient.first_name = '_Test IPD Patient' + patient.sex = 'Female' + patient.save(ignore_permissions=True) + patient = patient.name + return patient diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index 5b3f4c705ad..bf1ecc87e4c 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -137,13 +137,13 @@ var get_lab_test_prescribed = function(frm){ }); } else{ - frappe.msgprint(__("Please select Patient to get Lab Tests")); + frappe.msgprint(__("Please select a Patient to get Lab Tests")); } }; var show_lab_tests = function(frm, result){ var d = new frappe.ui.Dialog({ - title: __("Lab Test Prescriptions"), + title: __("Lab Tests"), fields: [ { fieldtype: "HTML", fieldname: "lab_test" @@ -161,7 +161,7 @@ var show_lab_tests = function(frm, result){
\
', {name:y[0], lab_test: y[1], encounter:y[2], invoiced:y[3], practitioner:y[4], date:y[5]})).appendTo(html_field); row.find("a").click(function() { frm.doc.template = $(this).attr("data-lab-test"); @@ -180,9 +180,10 @@ var show_lab_tests = function(frm, result){ return false; }); }); - if(!result){ - var msg = "There are no Lab Test prescribed for "+frm.doc.patient; - $(repl('
%(msg)s
', {msg: msg})).appendTo(html_field); + if(!result.length){ + var msg = __("No Lab Tests found for the Patient {0}", [frm.doc.patient_name.bold()]); + html_field.empty(); + $(repl('
%(msg)s
', {msg: msg})).appendTo(html_field); } d.show(); }; diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index ccbc24b3fb6..17dc1edd8c1 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -9,18 +9,18 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ - "inpatient_record", "naming_series", - "invoiced", "patient", "patient_name", "patient_age", "patient_sex", - "practitioner", + "report_preference", "email", "mobile", - "company", + "practitioner", "c_b", + "inpatient_record", + "company", "department", "status", "submitted_date", @@ -31,7 +31,7 @@ "employee_name", "employee_designation", "user", - "report_preference", + "invoiced", "sb_first", "lab_test_name", "column_break_26", @@ -153,7 +153,7 @@ { "fieldname": "company", "fieldtype": "Link", - "hidden": 1, + "in_standard_filter": 1, "label": "Company", "options": "Company", "print_hide": 1, @@ -168,6 +168,7 @@ "fieldname": "department", "fieldtype": "Link", "ignore_user_permissions": 1, + "in_standard_filter": 1, "label": "Department", "options": "Medical Department", "search_index": 1 @@ -427,7 +428,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-23 19:37:06.617764", + "modified": "2020-04-04 19:16:29.131168", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index ea8ce25c975..b2c5e6bf437 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -69,9 +69,9 @@ def create_multiple(doctype, docname): lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_("Lab Test(s) "+lab_test_created+" created.")) + frappe.msgprint(_("Lab Test(s) {0} created".format(lab_test_created))) else: - frappe.msgprint(_("No Lab Test created")) + frappe.msgprint(_("No Lab Tests created")) def create_lab_test_from_encounter(encounter_id): lab_test_created = False @@ -87,7 +87,7 @@ def create_lab_test_from_encounter(encounter_id): for lab_test_id in lab_test_ids: template = get_lab_test_template(lab_test_id[1]) if template: - lab_test = create_lab_test_doc(lab_test_id[2], encounter.practitioner, patient, template) + lab_test = create_lab_test_doc(lab_test_id[2], encounter.practitioner, patient, template, encounter.company) lab_test.save(ignore_permissions = True) frappe.db.set_value("Lab Prescription", lab_test_id[0], "lab_test_created", 1) if not lab_test_created: @@ -111,7 +111,7 @@ def create_lab_test_from_invoice(invoice_name): if lab_test_created != 1: template = get_lab_test_template(item.item_code) if template: - lab_test = create_lab_test_doc(True, invoice.ref_practitioner, patient, template) + lab_test = create_lab_test_doc(True, invoice.ref_practitioner, patient, template, invoice.company) if item.reference_dt == "Lab Prescription": lab_test.prescription = item.reference_dn lab_test.save(ignore_permissions = True) @@ -121,7 +121,7 @@ def create_lab_test_from_invoice(invoice_name): if not lab_tests_created: lab_tests_created = lab_test.name else: - lab_tests_created += ", "+lab_test.name + lab_tests_created += ", " + lab_test.name return lab_tests_created def get_lab_test_template(item): @@ -141,7 +141,7 @@ def check_template_exists(item): return template_exists return False -def create_lab_test_doc(invoiced, practitioner, patient, template): +def create_lab_test_doc(invoiced, practitioner, patient, template, company): lab_test = frappe.new_doc("Lab Test") lab_test.invoiced = invoiced lab_test.practitioner = practitioner @@ -150,11 +150,12 @@ def create_lab_test_doc(invoiced, practitioner, patient, template): lab_test.patient_sex = patient.sex lab_test.email = patient.email lab_test.mobile = patient.mobile + lab_test.report_preference = patient.report_preference lab_test.department = template.department lab_test.template = template.name lab_test.lab_test_group = template.lab_test_group lab_test.result_date = getdate() - lab_test.report_preference = patient.report_preference + lab_test.company = company return lab_test def create_normals(template, lab_test): @@ -190,7 +191,7 @@ def create_specials(template, lab_test): special.require_result_value = 1 special.template = template.name -def create_sample_doc(template, patient, invoice): +def create_sample_doc(template, patient, invoice, company = None): if template.sample: sample_exists = frappe.db.exists({ "doctype": "Sample Collection", @@ -221,6 +222,8 @@ def create_sample_doc(template, patient, invoice): sample_collection.sample = template.sample sample_collection.sample_uom = template.sample_uom sample_collection.sample_qty = template.sample_qty + sample_collection.company = company + if(template.sample_details): sample_collection.sample_details = "Test :" + (template.get("lab_test_name") or template.get("template")) +"\n"+"Collection Detials:\n\t"+template.sample_details sample_collection.save(ignore_permissions=True) @@ -229,7 +232,7 @@ def create_sample_doc(template, patient, invoice): def create_sample_collection(lab_test, template, patient, invoice): if(frappe.db.get_value("Healthcare Settings", None, "create_sample_collection_for_lab_test") == "1"): - sample_collection = create_sample_doc(template, patient, invoice) + sample_collection = create_sample_doc(template, patient, invoice, lab_test.company) if(sample_collection): lab_test.sample = sample_collection.name return lab_test diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index d5df9567ec5..490f2475001 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -10,6 +10,8 @@ frappe.ui.form.on('Patient', { ] }; }); + frm.set_query('customer_group', {'is_group': 0}); + frm.set_query('default_price_list', { 'selling': 1}); if (frappe.defaults.get_default('patient_name_by') != 'Naming Series') { frm.toggle_display('naming_series', false); @@ -40,6 +42,7 @@ frappe.ui.form.on('Patient', { frm.add_custom_button(__('Patient Encounter'), function () { create_encounter(frm); }, 'Create'); + frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option } }, onload: function (frm) { diff --git a/erpnext/healthcare/doctype/patient/patient.json b/erpnext/healthcare/doctype/patient/patient.json index 4258e4011de..8af1a9ccd75 100644 --- a/erpnext/healthcare/doctype/patient/patient.json +++ b/erpnext/healthcare/doctype/patient/patient.json @@ -24,13 +24,20 @@ "image", "column_break_14", "status", - "inpatient_status", "inpatient_record", - "customer", + "inpatient_status", + "report_preference", "mobile", "email", "phone", - "report_preference", + "customer_details_section", + "customer", + "customer_group", + "territory", + "column_break_24", + "default_currency", + "default_price_list", + "language", "personal_and_social_history", "occupation", "column_break_25", @@ -52,9 +59,7 @@ "surrounding_factors", "other_risk_factors", "more_info", - "patient_details", - "ac_sb", - "default_currency" + "patient_details" ], "fields": [ { @@ -67,6 +72,7 @@ { "fieldname": "inpatient_status", "fieldtype": "Select", + "in_preview": 1, "label": "Inpatient Status", "options": "\nAdmission Scheduled\nAdmitted\nDischarge Scheduled", "read_only": 1 @@ -101,6 +107,7 @@ { "fieldname": "sex", "fieldtype": "Link", + "in_preview": 1, "label": "Gender", "options": "Gender", "reqd": 1 @@ -109,6 +116,7 @@ "bold": 1, "fieldname": "blood_group", "fieldtype": "Select", + "in_preview": 1, "label": "Blood Group", "options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative" }, @@ -116,6 +124,7 @@ "bold": 1, "fieldname": "dob", "fieldtype": "Date", + "in_preview": 1, "label": "Date of birth" }, { @@ -142,6 +151,7 @@ "fieldname": "image", "fieldtype": "Attach Image", "hidden": 1, + "in_preview": 1, "label": "Image", "no_copy": 1, "print_hide": 1, @@ -157,7 +167,8 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Customer", - "options": "Customer" + "options": "Customer", + "set_only_once": 1 }, { "fieldname": "report_preference", @@ -171,7 +182,8 @@ "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, - "label": "Mobile" + "label": "Mobile", + "options": "Phone" }, { "bold": 1, @@ -186,7 +198,8 @@ "fieldname": "phone", "fieldtype": "Data", "in_filter": 1, - "label": "Phone" + "label": "Phone", + "options": "Phone" }, { "collapsible": 1, @@ -268,25 +281,25 @@ "fieldname": "tobacco_past_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Tobacco Consumption Habbits (Past)" + "label": "Tobacco Consumption (Past)" }, { "fieldname": "tobacco_current_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Tobacco Consumption Habbits (Present)" + "label": "Tobacco Consumption (Present)" }, { "fieldname": "alcohol_past_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Alcohol Consumption Habbits (Past)" + "label": "Alcohol Consumption (Past)" }, { "fieldname": "alcohol_current_use", "fieldtype": "Data", "ignore_user_permissions": 1, - "label": "Alcohol Consumption Habbits (Present)" + "label": "Alcohol Consumption (Present)" }, { "fieldname": "column_break_32", @@ -320,20 +333,11 @@ "ignore_xss_filter": 1, "label": "Patient Details" }, - { - "collapsible": 1, - "fieldname": "ac_sb", - "fieldtype": "Section Break", - "label": "Account Details" - }, { "fieldname": "default_currency", "fieldtype": "Link", - "hidden": 1, - "ignore_xss_filter": 1, - "label": "Default Currency", - "options": "Currency", - "print_hide": 1 + "label": "Billing Currency", + "options": "Currency" }, { "fieldname": "last_name", @@ -351,13 +355,47 @@ "fieldname": "middle_name", "fieldtype": "Data", "label": "Middle Name (optional)" + }, + { + "collapsible": 1, + "fieldname": "customer_details_section", + "fieldtype": "Section Break", + "label": "Customer Details" + }, + { + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, + { + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "options": "Territory" + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_price_list", + "fieldtype": "Link", + "label": "Default Price List", + "options": "Price List" + }, + { + "fieldname": "language", + "fieldtype": "Link", + "label": "Print Language", + "options": "Language" } ], "icon": "fa fa-user", "image_field": "image", "links": [], "max_attachments": 50, - "modified": "2020-04-06 12:55:30.807744", + "modified": "2020-04-25 17:24:32.146415", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient", diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index e304a0bbc36..30a1e45f0ee 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -10,6 +10,7 @@ from frappe.utils import cint, cstr, getdate import dateutil from frappe.model.naming import set_name_by_naming_series from frappe.utils.nestedset import get_root_of +from erpnext import get_default_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account, send_registration_sms class Patient(Document): @@ -17,6 +18,9 @@ class Patient(Document): self.set_full_name() self.add_as_website_user() + def before_insert(self): + self.set_missing_customer_details() + def after_insert(self): self.add_as_website_user() self.reload() @@ -26,6 +30,25 @@ class Patient(Document): frappe.db.set_value('Patient', self.name, 'status', 'Disabled') else: send_registration_sms(self) + self.reload() # self.notify_update() + + def on_update(self): + if self.customer: + customer = frappe.get_doc('Customer', self.customer) + if self.customer_group: + customer.customer_group = self.customer_group + if self.territory: + customer.territory = self.territory + + customer.customer_name = self.patient_name + customer.default_price_list = self.default_price_list + customer.default_currency = self.default_currency + customer.language = self.language + customer.ignore_mandatory = True + customer.save(ignore_permissions=True) + else: + if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): + create_customer(self) def set_full_name(self): if self.last_name: @@ -33,6 +56,22 @@ class Patient(Document): else: self.patient_name = self.first_name + def set_missing_customer_details(self): + if not self.customer_group: + self.customer_group = frappe.db.get_single_value('Selling Settings', 'customer_group') or get_root_of('Customer Group') + if not self.territory: + self.territory = frappe.db.get_single_value('Selling Settings', 'territory') or get_root_of('Territory') + if not self.default_price_list: + self.default_price_list = frappe.db.get_single_value('Selling Settings', 'selling_price_list') + + if not self.customer_group or not self.territory or not self.default_price_list: + frappe.msgprint(_('Please set defaults for Customer Group, Territory and Selling Price List in Selling Settings'), alert=True) + + if not self.default_currency: + self.default_currency = get_default_currency() + if not self.language: + self.language = frappe.db.get_single_value('System Settings', 'language') + def add_as_website_user(self): if self.email: if not frappe.db.exists ('User', self.email): @@ -86,19 +125,15 @@ class Patient(Document): return {'invoice': sales_invoice.name} def create_customer(doc): - customer_group = frappe.db.get_single_value('Selling Settings', 'customer_group') - territory = frappe.db.get_single_value('Selling Settings', 'territory') - if not (customer_group and territory): - customer_group = get_root_of('Customer Group') - territory = get_root_of('Territory') - frappe.msgprint(_('Please set default customer group and territory in Selling Settings'), alert=True) - customer = frappe.get_doc({ 'doctype': 'Customer', 'customer_name': doc.patient_name, - 'customer_group': customer_group, - 'territory' : territory, - 'customer_type': 'Individual' + 'customer_group': doc.customer_group or frappe.db.get_single_value('Selling Settings', 'customer_group'), + 'territory' : doc.territory or frappe.db.get_single_value('Selling Settings', 'territory'), + 'customer_type': 'Individual', + 'default_currency': doc.default_currency, + 'default_price_list': doc.default_price_list, + 'language': doc.language }).insert(ignore_permissions=True, ignore_mandatory=True) frappe.db.set_value('Patient', doc.name, 'customer', customer.name) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index fa589347ffa..f7ed31bfeab 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -32,8 +32,9 @@ frappe.ui.form.on('Patient Appointment', { frm.set_query('service_unit', function(){ return { filters: { - 'is_group': 0, - 'allow_appointments': 1 + 'is_group': false, + 'allow_appointments': true, + 'company': frm.doc.company } }; }); @@ -127,6 +128,11 @@ frappe.ui.form.on('Patient Appointment', { patient: function(frm) { if (frm.doc.patient) { frm.trigger('toggle_payment_fields'); + } else { + frm.set_value('patient_name', ''); + frm.set_value('patient_sex', ''); + frm.set_value('patient_age', ''); + frm.set_value('inpatient_record', ''); } }, @@ -230,7 +236,6 @@ let check_and_set_availability = function(frm) { d.hide(); frm.enable_save(); frm.save(); - frm.enable_save(); d.get_primary_btn().attr('disabled', true); } }); @@ -481,6 +486,7 @@ let create_vital_signs = function(frm) { frappe.route_options = { 'patient': frm.doc.patient, 'appointment': frm.doc.name, + 'company': frm.doc.company }; frappe.new_doc('Vital Signs'); }; @@ -513,6 +519,7 @@ frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) { 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); } }); } diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 57e6c479d1e..b8a400c6b7a 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -10,40 +10,44 @@ "engine": "InnoDB", "field_order": [ "naming_series", + "title", + "status", "patient", "patient_name", "patient_sex", "patient_age", "inpatient_record", "column_break_1", - "status", + "company", + "service_unit", "procedure_template", "get_procedure_from_encounter", "procedure_prescription", "therapy_type", "get_prescribed_therapies", "therapy_plan", - "service_unit", - "section_break_12", "practitioner", + "practitioner_name", "department", + "section_break_12", "appointment_type", + "duration", "column_break_17", "appointment_date", "appointment_time", "appointment_datetime", - "duration", "section_break_16", "mode_of_payment", - "paid_amount", - "company", + "billing_item", "column_break_2", + "paid_amount", "invoiced", "ref_sales_invoice", "section_break_3", - "notes", "referring_practitioner", - "reminded" + "reminded", + "column_break_36", + "notes" ], "fields": [ { @@ -55,7 +59,6 @@ "read_only": 1 }, { - "fetch_from": "inpatient_record.patient", "fieldname": "patient", "fieldtype": "Link", "ignore_user_permissions": 1, @@ -79,7 +82,8 @@ "fieldname": "duration", "fieldtype": "Int", "in_filter": 1, - "label": "Duration (In Minutes)" + "label": "Duration (In Minutes)", + "set_only_once": 1 }, { "fieldname": "column_break_1", @@ -98,6 +102,7 @@ "search_index": 1 }, { + "depends_on": "eval:doc.patient;", "fieldname": "procedure_template", "fieldtype": "Link", "label": "Clinical Procedure Template", @@ -117,7 +122,8 @@ "label": "Procedure Prescription", "no_copy": 1, "options": "Procedure Prescription", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "service_unit", @@ -128,7 +134,8 @@ }, { "fieldname": "section_break_12", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Appointment Details" }, { "fieldname": "practitioner", @@ -143,6 +150,7 @@ "set_only_once": 1 }, { + "fetch_from": "practitioner.department", "fieldname": "department", "fieldtype": "Link", "ignore_user_permissions": 1, @@ -173,11 +181,13 @@ "fieldtype": "Time", "in_list_view": 1, "label": "Time", - "read_only": 1 + "read_only": 1, + "reqd": 1 }, { "fieldname": "section_break_16", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Payments" }, { "fetch_from": "patient.patient_name", @@ -206,6 +216,7 @@ { "fieldname": "appointment_datetime", "fieldtype": "Datetime", + "hidden": 1, "label": "Appointment Datetime", "print_hide": 1, "read_only": 1, @@ -237,12 +248,12 @@ { "fieldname": "company", "fieldtype": "Link", - "hidden": 1, + "in_standard_filter": 1, "label": "Company", "no_copy": 1, "options": "Company", - "print_hide": 1, - "report_hide": 1 + "reqd": 1, + "set_only_once": 1 }, { "collapsible": 1, @@ -307,10 +318,37 @@ "label": "Series", "options": "HLC-APP-.YYYY.-", "set_only_once": 1 + }, + { + "fieldname": "billing_item", + "fieldtype": "Link", + "label": "Billing Item", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "practitioner.practitioner_name", + "fieldname": "practitioner_name", + "fieldtype": "Data", + "label": "Practitioner Name", + "read_only": 1 } ], "links": [], - "modified": "2020-03-31 16:16:32.116865", + "modified": "2020-04-27 21:36:06.404062", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", @@ -358,7 +396,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "patient", + "title_field": "title", "track_changes": 1, "track_seen": 1 } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index c4ec30f0469..9eb6e77c855 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -21,6 +21,7 @@ class PatientAppointment(Document): self.set_appointment_datetime() self.validate_customer_created() self.set_status() + self.set_title() def after_insert(self): self.update_prescription_details() @@ -28,6 +29,10 @@ class PatientAppointment(Document): self.update_fee_validity() send_confirmation_msg(self) + def set_title(self): + self.title = _('{0} with {1}').format(self.patient_name or self.patient, + self.practitioner_name or self.practitioner) + def set_status(self): today = getdate() appointment_date = getdate(self.appointment_date) @@ -119,25 +124,28 @@ def invoice_appointment(appointment_doc): 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.is_pos = 1 sales_invoice.company = appointment_doc.company sales_invoice.debit_to = get_receivable_account(appointment_doc.company) item = sales_invoice.append('items', {}) item = get_appointment_item(appointment_doc, item) - payment = sales_invoice.append('payments', {}) - payment.mode_of_payment = appointment_doc.mode_of_payment - payment.amount = appointment_doc.paid_amount + # 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 as paid'.format(sales_invoice.name)), alert=True) + frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) @@ -343,8 +351,8 @@ def make_encounter(source_name, target_doc=None): ['practitioner', 'practitioner'], ['medical_department', 'department'], ['patient_sex', 'patient_sex'], - ['encounter_date', 'appointment_date'], - ['invoiced', 'invoiced'] + ['invoiced', 'invoiced'], + ['company', 'company'] ] } }, target_doc) @@ -370,17 +378,19 @@ def send_appointment_reminder(): frappe.db.set_value('Patient Appointment', doc.name, 'reminded', 1) def send_message(doc, message): - patient = frappe.get_doc('Patient', doc.patient) - if patient.mobile: + patient_mobile = frappe.db.get_value('Patient', doc.patient, 'mobile') + if patient_mobile: context = {'doc': doc, 'alert': doc, 'comments': None} if doc.get('_comments'): context['comments'] = json.loads(doc.get('_comments')) # jinja to string convertion happens here message = frappe.render_template(message, context) - number = [patient.mobile] - send_sms(number, message) - + number = [patient_mobile] + try: + send_sms(number, message) + except Exception as e: + frappe.msgprint(_('SMS not sent, please check SMS Settings'), alert=True) @frappe.whitelist() def get_events(start, end, filters=None): diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 7075af5d00c..eeed157291e 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -4,14 +4,15 @@ from __future__ import unicode_literals import unittest import frappe -from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status +from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter from frappe.utils import nowdate, add_days from frappe.utils.make_random import get_random class TestPatientAppointment(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabPatient Appointment`""") - frappe.db.sql("""delete from `tabFee Validity""") + frappe.db.sql("""delete from `tabFee Validity`""") + frappe.db.sql("""delete from `tabPatient Encounter`""") def test_status(self): patient, medical_department, practitioner = create_healthcare_docs() @@ -23,6 +24,19 @@ class TestPatientAppointment(unittest.TestCase): create_encounter(appointment) self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + def test_start_encounter(self): + 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) + encounter = make_encounter(appointment.name) + self.assertTrue(encounter) + self.assertEqual(encounter.company, appointment.company) + self.assertEqual(encounter.practitioner, appointment.practitioner) + self.assertEqual(encounter.patient, appointment.patient) + # invoiced flag mapped from appointment + self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) + def test_invoicing(self): patient, medical_department, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) @@ -33,7 +47,11 @@ class TestPatientAppointment(unittest.TestCase): frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1) self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1) - self.assertTrue(frappe.db.get_value('Patient Appointment', appointment.name, 'ref_sales_invoice')) + 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, 'company'), appointment.company) + 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_appointment_cancel(self): patient, medical_department, practitioner = create_healthcare_docs() @@ -53,8 +71,8 @@ class TestPatientAppointment(unittest.TestCase): appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) update_status(appointment.name, 'Cancelled') # check invoice cancelled - sales_invoice = frappe.db.get_value('Patient Appointment', appointment.name, 'ref_sales_invoice') - self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice, 'status'), 'Cancelled') + sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') + self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled') def create_healthcare_docs(): @@ -90,14 +108,15 @@ def create_patient(): patient = patient.name return patient -def create_encounter(appointment=None): - encounter = frappe.new_doc('Patient Encounter') +def create_encounter(appointment): if appointment: + encounter = frappe.new_doc('Patient Encounter') encounter.appointment = appointment.name encounter.patient = appointment.patient encounter.practitioner = appointment.practitioner encounter.encounter_date = appointment.appointment_date encounter.encounter_time = appointment.appointment_time + encounter.company = appointment.company encounter.save() encounter.submit() return encounter diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index 78e789d3594..2410f8e10dd 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -25,15 +25,16 @@ frappe.ui.form.on('Patient Encounter', { refresh_field('lab_test_prescription'); if (!frm.doc.__islocal) { - - if (frm.doc.inpatient_status == 'Admission Scheduled' || frm.doc.inpatient_status == 'Admitted') { - frm.add_custom_button(__('Schedule Discharge'), function() { - schedule_discharge(frm); - }); - } else if (frm.doc.inpatient_status != 'Discharge Scheduled') { - frm.add_custom_button(__('Schedule Admission'), function() { - schedule_inpatient(frm); - }); + if (frm.doc.docstatus === 1) { + if (frm.doc.inpatient_status == 'Admission Scheduled' || frm.doc.inpatient_status == 'Admitted') { + frm.add_custom_button(__('Schedule Discharge'), function() { + schedule_discharge(frm); + }); + } else if (frm.doc.inpatient_status != 'Discharge Scheduled') { + frm.add_custom_button(__('Schedule Admission'), function() { + schedule_inpatient(frm); + }); + } } frm.add_custom_button(__('Patient History'), function() { @@ -101,6 +102,11 @@ frappe.ui.form.on('Patient Encounter', { frm.events.set_patient_info(frm); }, + practitioner: function(frm) { + if (!frm.doc.practitioner) { + frm.set_value('practitioner_name', ''); + } + }, set_appointment_fields: function(frm) { if (frm.doc.appointment) { frappe.call({ @@ -114,9 +120,11 @@ frappe.ui.form.on('Patient Encounter', { 'patient':data.message.patient, 'type': data.message.appointment_type, 'practitioner': data.message.practitioner, - 'invoiced': data.message.invoiced + 'invoiced': data.message.invoiced, + 'company': data.message.company }; frm.set_value(values); + frm.set_df_property('patient', 'read_only', 1); } }); } @@ -133,6 +141,7 @@ frappe.ui.form.on('Patient Encounter', { 'inpatient_status': '' }; frm.set_value(values); + frm.set_df_property('patient', 'read_only', 0); } }, @@ -148,19 +157,25 @@ frappe.ui.form.on('Patient Encounter', { if (data.message.dob) { age = calculate_age(data.message.dob); } - frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age); - frappe.model.set_value(frm.doctype, frm.docname, 'patient_sex', data.message.sex); - if (data.message.inpatient_record) { - frappe.model.set_value(frm.doctype, frm.docname, 'inpatient_record', data.message.inpatient_record); - frappe.model.set_value(frm.doctype, frm.docname, 'inpatient_status', data.message.inpatient_status); - } + let values = { + 'patient_age': age, + 'patient_name':data.message.patient_name, + 'patient_sex': data.message.sex, + 'inpatient_record': data.message.inpatient_record, + 'inpatient_status': data.message.inpatient_status + }; + frm.set_value(values); } }); } else { - frappe.model.set_value(frm.doctype, frm.docname, 'patient_sex', ''); - frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', ''); - frappe.model.set_value(frm.doctype, frm.docname, 'inpatient_record', ''); - frappe.model.set_value(frm.doctype, frm.docname, 'inpatient_status', ''); + let values = { + 'patient_age': '', + 'patient_name':'', + 'patient_sex': '', + 'inpatient_record': '', + 'inpatient_status': '' + }; + frm.set_value(values); } } }); @@ -212,8 +227,8 @@ let create_vital_signs = function (frm) { } frappe.route_options = { 'patient': frm.doc.patient, - 'appointment': frm.doc.appointment, - 'encounter': frm.doc.name + 'encounter': frm.doc.name, + 'company': frm.doc.company }; frappe.new_doc('Vital Signs'); }; @@ -224,7 +239,8 @@ let create_procedure = function (frm) { } frappe.route_options = { 'patient': frm.doc.patient, - 'medical_department': frm.doc.medical_department + 'medical_department': frm.doc.medical_department, + 'company': frm.doc.company }; frappe.new_doc('Clinical Procedure'); }; diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index 5f11039e19b..05eec87398b 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json @@ -11,23 +11,23 @@ "engine": "InnoDB", "field_order": [ "naming_series", + "title", "appointment", "appointment_type", "patient", "patient_name", "patient_sex", "patient_age", - "company", + "inpatient_record", + "inpatient_status", "column_break_6", - "practitioner", - "medical_department", + "company", "encounter_date", "encounter_time", + "practitioner", + "practitioner_name", + "medical_department", "invoiced", - "section_break_1", - "inpatient_record", - "column_break_17", - "inpatient_status", "sb_symptoms", "symptoms", "symptoms_in_print", @@ -47,6 +47,7 @@ "therapies", "section_break_33", "encounter_comment", + "sb_refs", "amended_from" ], "fields": [ @@ -57,12 +58,6 @@ "options": "Inpatient Record", "read_only": 1 }, - { - "collapsible": 1, - "fieldname": "section_break_1", - "fieldtype": "Section Break", - "label": "Inpatient Details" - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -77,14 +72,13 @@ "ignore_user_permissions": 1, "label": "Appointment", "options": "Patient Appointment", - "search_index": 1 + "search_index": 1, + "set_only_once": 1 }, { - "fetch_from": "inpatient_record.patient", "fieldname": "patient", "fieldtype": "Link", "ignore_user_permissions": 1, - "in_list_view": 1, "in_standard_filter": 1, "label": "Patient", "options": "Patient", @@ -92,7 +86,6 @@ "search_index": 1 }, { - "fetch_from": "patient.patient_name", "fieldname": "patient_name", "fieldtype": "Data", "label": "Patient Name", @@ -114,7 +107,6 @@ { "fieldname": "company", "fieldtype": "Link", - "hidden": 1, "label": "Company", "options": "Company" }, @@ -125,7 +117,6 @@ { "fieldname": "practitioner", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", @@ -207,29 +198,29 @@ { "fieldname": "codification_table", "fieldtype": "Table", - "label": "Medical Coding", + "label": "Medical Codes", "options": "Codification Table" }, { "fieldname": "sb_drug_prescription", "fieldtype": "Section Break", - "label": "Medication" + "label": "Medications" }, { "fieldname": "drug_prescription", "fieldtype": "Table", - "label": "Drug Prescription", + "label": "Items", "options": "Drug Prescription" }, { "fieldname": "sb_test_prescription", "fieldtype": "Section Break", - "label": "Investigation" + "label": "Investigations" }, { "fieldname": "lab_test_prescription", "fieldtype": "Table", - "label": "Lab Prescription", + "label": "Lab Tests", "options": "Lab Prescription" }, { @@ -240,7 +231,7 @@ { "fieldname": "procedure_prescription", "fieldtype": "Table", - "label": "Procedure Prescription", + "label": "Clinical Procedures", "no_copy": 1, "options": "Procedure Prescription" }, @@ -299,7 +290,6 @@ "fieldname": "medical_department", "fieldtype": "Link", "ignore_user_permissions": 1, - "in_list_view": 1, "in_standard_filter": 1, "label": "Department", "options": "Medical Department", @@ -312,13 +302,31 @@ "read_only": 1 }, { - "fieldname": "column_break_17", - "fieldtype": "Column Break" + "fieldname": "sb_refs", + "fieldtype": "Section Break" + }, + { + "fetch_from": "practitioner.practitioner_name", + "fieldname": "practitioner_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Practitioner Name", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-14 16:18:08.180457", + "modified": "2020-04-27 21:58:29.789797", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Encounter", @@ -345,7 +353,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "patient", + "title_field": "title", "track_changes": 1, "track_seen": 1 } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 1734c28e52b..56401a3e742 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -10,6 +10,9 @@ from frappe.utils import cstr from frappe import _ class PatientEncounter(Document): + def validate(self): + self.set_title() + def on_update(self): if self.appointment: frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') @@ -29,6 +32,10 @@ class PatientEncounter(Document): def on_submit(self): create_therapy_plan(self) + def set_title(self): + self.title = _('{0} with {1}').format(self.patient_name or self.patient, + self.practitioner_name or self.practitioner)[:100] + def create_therapy_plan(encounter): if len(encounter.therapies): doc = frappe.new_doc('Therapy Plan') diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.json b/erpnext/healthcare/doctype/sample_collection/sample_collection.json index 39cead88621..c352287faf4 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.json +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.json @@ -9,14 +9,14 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ - "inpatient_record", "naming_series", - "invoiced", "patient", - "column_break_4", "patient_age", "patient_sex", + "column_break_4", + "inpatient_record", "company", + "invoiced", "section_break_6", "sample", "sample_uom", @@ -167,7 +167,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-25 16:55:52.376834", + "modified": "2020-04-04 19:17:02.707203", "modified_by": "Administrator", "module": "Healthcare", "name": "Sample Collection", diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js index abe4defaf93..e66e66751aa 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -9,6 +9,16 @@ frappe.ui.form.on('Therapy Session', { {fieldname: 'counts_completed', columns: 1}, {fieldname: 'assistance_level', columns: 1} ]; + + frm.set_query('service_unit', function() { + return { + filters: { + 'is_group': false, + 'allow_appointments': true, + 'company': frm.doc.company + } + }; + }); }, refresh: function(frm) { diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.json b/erpnext/healthcare/doctype/vital_signs/vital_signs.json index 75726dbe070..15ab5047bc4 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.json +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.json @@ -2,18 +2,22 @@ "actions": [], "allow_copy": 1, "allow_import": 1, + "autoname": "naming_series:", "beta": 1, "creation": "2017-02-02 11:00:24.853005", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "inpatient_record", + "naming_series", + "title", "patient", "patient_name", + "inpatient_record", "appointment", "encounter", "column_break_2", + "company", "signs_date", "signs_time", "sb_vs", @@ -34,7 +38,7 @@ "bmi", "column_break_14", "nutrition_note", - "company", + "sb_references", "amended_from" ], "fields": [ @@ -68,7 +72,8 @@ "fieldname": "appointment", "fieldtype": "Link", "in_filter": 1, - "label": "Appointment", + "label": "Patient Appointment", + "no_copy": 1, "options": "Patient Appointment", "print_hide": 1, "read_only": 1 @@ -81,8 +86,7 @@ "no_copy": 1, "options": "Patient Encounter", "print_hide": 1, - "read_only": 1, - "report_hide": 1 + "read_only": 1 }, { "fieldname": "column_break_2", @@ -217,7 +221,6 @@ { "fieldname": "company", "fieldtype": "Link", - "hidden": 1, "label": "Company", "options": "Company" }, @@ -229,11 +232,34 @@ "options": "Vital Signs", "print_hide": 1, "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "sb_references", + "fieldtype": "Section Break" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "HLC-VTS-.YYYY.-", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "columns": 5, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-03-04 17:19:29.549889", + "modified": "2020-05-17 22:23:24.632286", "modified_by": "Administrator", "module": "Healthcare", "name": "Vital Signs", @@ -273,7 +299,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "patient", + "title_field": "title", "track_changes": 1, "track_seen": 1 } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py index b0e78e8eb94..69d81ff4b08 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py @@ -9,12 +9,19 @@ from frappe.utils import cstr from frappe import _ class VitalSigns(Document): + def validate(self): + self.set_title() + def on_submit(self): insert_vital_signs_to_medical_record(self) def on_cancel(self): delete_vital_signs_from_medical_record(self) + def set_title(self): + self.title = _('{0} on {1}').format(self.patient_name or self.patient, + frappe.utils.format_date(self.signs_date))[:100] + def insert_vital_signs_to_medical_record(doc): subject = set_subject_field(doc) medical_record = frappe.new_doc('Patient Medical Record') diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index a7565323585..f092578003e 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -3,83 +3,84 @@ # For license information, please see license.txt from __future__ import unicode_literals +import math import frappe from frappe import _ -import math from frappe.utils import time_diff_in_hours, rounded from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple @frappe.whitelist() -def get_healthcare_services_to_invoice(patient): +def get_healthcare_services_to_invoice(patient, company): patient = frappe.get_doc('Patient', patient) + items_to_invoice = [] if patient: validate_customer_created(patient) - items_to_invoice = [] - patient_appointments = frappe.get_list( - 'Patient Appointment', - fields='*', - filters={'patient': patient.name, 'invoiced': 0}, - order_by='appointment_date' - ) - if patient_appointments: - items_to_invoice = get_fee_validity(patient_appointments) + # Customer validated, build a list of billable services + items_to_invoice += get_appointments_to_invoice(patient, company) + items_to_invoice += get_encounters_to_invoice(patient, company) + items_to_invoice += get_lab_tests_to_invoice(patient, company) + items_to_invoice += get_clinical_procedures_to_invoice(patient, company) + items_to_invoice += get_inpatient_services_to_invoice(patient, company) + items_to_invoice += get_therapy_sessions_to_invoice(patient, company) - encounters = get_encounters_to_invoice(patient) - lab_tests = get_lab_tests_to_invoice(patient) - clinical_procedures = get_clinical_procedures_to_invoice(patient) - inpatient_services = get_inpatient_services_to_invoice(patient) - therapy_sessions = get_therapy_sessions_to_invoice(patient) - items_to_invoice += encounters + lab_tests + clinical_procedures + inpatient_services + therapy_sessions return items_to_invoice + def validate_customer_created(patient): if not frappe.db.get_value('Patient', patient.name, 'customer'): msg = _("Please set a Customer linked to the Patient") msg += " {0}".format(patient.name) frappe.throw(msg, title=_('Customer Not Found')) -def get_fee_validity(patient_appointments): - if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): - return [] +def get_appointments_to_invoice(patient, company): + appointments_to_invoice = [] + patient_appointments = frappe.get_list( + 'Patient Appointment', + fields = '*', + filters = {'patient': patient.name, 'company': company, 'invoiced': 0}, + order_by = 'appointment_date' + ) - items_to_invoice = [] for appointment in patient_appointments: + # Procedure Appointments if appointment.procedure_template: if frappe.db.get_value('Clinical Procedure Template', appointment.procedure_template, 'is_billable'): - items_to_invoice.append({ + appointments_to_invoice.append({ 'reference_type': 'Patient Appointment', 'reference_name': appointment.name, 'service': appointment.procedure_template }) + # Consultation Appointments, should check fee validity else: - fee_validity = frappe.db.exists('Fee Validity Reference', {'appointment': appointment.name}) - if not fee_validity: - practitioner_charge = 0 - income_account = None - service_item = None - if appointment.practitioner: - service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment) - income_account = get_income_account(appointment.practitioner, appointment.company) - items_to_invoice.append({ - 'reference_type': 'Patient Appointment', - 'reference_name': appointment.name, - 'service': service_item, - 'rate': practitioner_charge, - 'income_account': income_account - }) + if frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups') and \ + frappe.db.exists('Fee Validity Reference', {'appointment': appointment.name}): + continue # Skip invoicing, fee validty present + practitioner_charge = 0 + income_account = None + service_item = None + if appointment.practitioner: + service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment) + income_account = get_income_account(appointment.practitioner, appointment.company) + appointments_to_invoice.append({ + 'reference_type': 'Patient Appointment', + 'reference_name': appointment.name, + 'service': service_item, + 'rate': practitioner_charge, + 'income_account': income_account + }) - return items_to_invoice + return appointments_to_invoice -def get_encounters_to_invoice(patient): +def get_encounters_to_invoice(patient, company): encounters_to_invoice = [] encounters = frappe.get_list( 'Patient Encounter', fields=['*'], - filters={'patient': patient.name, 'invoiced': False, 'docstatus': 1} + filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} ) if encounters: for encounter in encounters: @@ -102,12 +103,12 @@ def get_encounters_to_invoice(patient): return encounters_to_invoice -def get_lab_tests_to_invoice(patient): +def get_lab_tests_to_invoice(patient, company): lab_tests_to_invoice = [] lab_tests = frappe.get_list( 'Lab Test', fields=['name', 'template'], - filters={'patient': patient.name, 'invoiced': False, 'docstatus': 1} + filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} ) for lab_test in lab_tests: item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.template, ['item', 'is_billable']) @@ -143,12 +144,12 @@ def get_lab_tests_to_invoice(patient): return lab_tests_to_invoice -def get_clinical_procedures_to_invoice(patient): +def get_clinical_procedures_to_invoice(patient, company): clinical_procedures_to_invoice = [] procedures = frappe.get_list( 'Clinical Procedure', fields='*', - filters={'patient': patient.name, 'invoiced': False} + filters={'patient': patient.name, 'company': company, 'invoiced': False} ) for procedure in procedures: if not procedure.appointment: @@ -204,7 +205,7 @@ def get_clinical_procedures_to_invoice(patient): return clinical_procedures_to_invoice -def get_inpatient_services_to_invoice(patient): +def get_inpatient_services_to_invoice(patient, company): services_to_invoice = [] inpatient_services = frappe.db.sql( ''' @@ -214,10 +215,11 @@ def get_inpatient_services_to_invoice(patient): `tabInpatient Record` ip, `tabInpatient Occupancy` io WHERE ip.patient=%s + and ip.company=%s and io.parent=ip.name and io.left=1 and io.invoiced=0 - ''', (patient.name), as_dict=1) + ''', (patient.name, company), as_dict=1) for inpatient_occupancy in inpatient_services: service_unit_type = frappe.db.get_value('Healthcare Service Unit', inpatient_occupancy.service_unit, 'service_unit_type') @@ -244,12 +246,12 @@ def get_inpatient_services_to_invoice(patient): return services_to_invoice -def get_therapy_sessions_to_invoice(patient): +def get_therapy_sessions_to_invoice(patient, company): therapy_sessions_to_invoice = [] therapy_sessions = frappe.get_list( 'Therapy Session', fields='*', - filters={'patient': patient.name, 'invoiced': False} + filters={'patient': patient.name, 'invoiced': 0, 'company': company} ) for therapy in therapy_sessions: if not therapy.appointment: @@ -396,6 +398,7 @@ def check_fee_validity(appointment): def manage_fee_validity(appointment): fee_validity = check_fee_validity(appointment) + if fee_validity: if appointment.status == 'Cancelled' and fee_validity.visited > 0: fee_validity.visited -= 1 diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index b6c80655c2e..6c7ee002db4 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -21,7 +21,7 @@ class Attendance(Document): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") # leaves can be marked for future dates - if self.status not in ('On Leave', 'Half Day') and getdate(self.attendance_date) > getdate(nowdate()): + if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()): frappe.throw(_("Attendance can not be marked for future dates")) elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) @@ -41,7 +41,7 @@ class Attendance(Document): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date from `tabLeave Application` - where employee = %s + where employee = %s and %s between from_date and to_date and status = 'Approved' and docstatus = 1 diff --git a/erpnext/hr/doctype/department/department.json b/erpnext/hr/doctype/department/department.json index 6469f4cbed2..a54c1d18e7c 100644 --- a/erpnext/hr/doctype/department/department.json +++ b/erpnext/hr/doctype/department/department.json @@ -14,6 +14,8 @@ "is_group", "disabled", "section_break_4", + "payroll_cost_center", + "column_break_9", "leave_block_list", "leave_section", "leave_approvers", @@ -125,13 +127,23 @@ { "fieldname": "column_break_3", "fieldtype": "Column Break" + }, + { + "fieldname": "payroll_cost_center", + "fieldtype": "Link", + "label": "Payroll Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" } ], "icon": "fa fa-sitemap", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:03:27.784362", + "modified": "2020-05-05 18:49:28.503931", "modified_by": "Administrator", "module": "HR", "name": "Department", diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 13c202c7759..f575765f69b 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -60,6 +60,8 @@ "default_shift", "salary_information", "salary_mode", + "payroll_cost_center", + "column_break_52", "bank_name", "bank_ac_no", "health_insurance_section", @@ -783,13 +785,25 @@ { "fieldname": "column_break_19", "fieldtype": "Column Break" + }, + { + "fetch_from": "department.payroll_cost_center", + "fetch_if_empty": 1, + "fieldname": "payroll_cost_center", + "fieldtype": "Link", + "label": "Payroll Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "column_break_52", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2020-04-08 12:25:34.306695", + "modified": "2020-05-05 18:51:03.152503", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index d3410de2eb7..f4b214adc3c 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -45,7 +45,7 @@ class TestEmployee(unittest.TestCase): employee1_doc.status = 'Left' self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) -def make_employee(user, company=None): +def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): frappe.get_doc({ "doctype": "User", @@ -55,7 +55,7 @@ def make_employee(user, company=None): "roles": [{"doctype": "Has Role", "role": "Employee"}] }).insert() - if not frappe.db.get_value("Employee", { "user_id": user, "company": company or erpnext.get_default_company() }): + if not frappe.db.get_value("Employee", {"user_id": user}): employee = frappe.get_doc({ "doctype": "Employee", "naming_series": "EMP-", @@ -71,7 +71,10 @@ def make_employee(user, company=None): "prefered_email": user, "status": "Active", "employment_type": "Intern" - }).insert() + }) + if kwargs: + employee.update(kwargs) + employee.insert() return employee.name else: return frappe.get_value("Employee", {"employee_name":user}, "name") diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index 9ef3a99930b..656de0170dd 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -55,6 +55,7 @@ class PayrollEntry(Document): ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s {condition}""".format(condition=condition), {"company": self.company, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) + if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and %(from_date)s >= t2.from_date" @@ -138,7 +139,7 @@ class PayrollEntry(Document): cond = self.get_filter_condition() ss_list = frappe.db.sql(""" - select t1.name, t1.salary_structure from `tabSalary Slip` t1 + select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1 where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s %s """ % ('%s', '%s', '%s','%s', cond), (ss_status, self.start_date, self.end_date, self.salary_slip_based_on_timesheet), as_dict=as_dict) @@ -169,10 +170,14 @@ class PayrollEntry(Document): def get_salary_components(self, component_type): salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True) - if salary_slips: - salary_components = frappe.db.sql("""select salary_component, amount, parentfield - from `tabSalary Detail` where parentfield = '%s' and parent in (%s)""" % - (component_type, ', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=True) + if salary_slips: + salary_components = frappe.db.sql(""" + select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center + from `tabSalary Slip` ss, `tabSalary Detail` ssd + where ss.name = ssd.parent and ssd.parentfield = '%s' and ss.name in (%s) + """ % (component_type, ', '.join(['%s']*len(salary_slips))), + tuple([d.name for d in salary_slips]), as_dict=True) + return salary_components def get_salary_component_total(self, component_type = None): @@ -186,15 +191,16 @@ class PayrollEntry(Document): if is_flexible_benefit == 1 and only_tax_impact ==1: add_component_to_accrual_jv_entry = False if add_component_to_accrual_jv_entry: - component_dict[item['salary_component']] = component_dict.get(item['salary_component'], 0) + item['amount'] + component_dict[(item.salary_component, item.payroll_cost_center)] \ + = component_dict.get((item.salary_component, item.payroll_cost_center), 0) + flt(item.amount) account_details = self.get_account(component_dict = component_dict) return account_details def get_account(self, component_dict = None): - account_dict = {} - for s, a in component_dict.items(): - account = self.get_salary_component_account(s) - account_dict[account] = account_dict.get(account, 0) + a + account_dict = {} + for key, amount in component_dict.items(): + account = self.get_salary_component_account(key[0]) + account_dict[(account, key[1])] = account_dict.get((account, key[1]), 0) + amount return account_dict def get_default_payroll_payable_account(self): @@ -227,23 +233,23 @@ class PayrollEntry(Document): payable_amount = 0 # Earnings - for acc, amount in earnings.items(): + for acc_cc, amount in earnings.items(): payable_amount += flt(amount, precision) accounts.append({ - "account": acc, + "account": acc_cc[0], "debit_in_account_currency": flt(amount, precision), "party_type": '', - "cost_center": self.cost_center, + "cost_center": acc_cc[1] or self.cost_center, "project": self.project }) # Deductions - for acc, amount in deductions.items(): + for acc_cc, amount in deductions.items(): payable_amount -= flt(amount, precision) accounts.append({ - "account": acc, + "account": acc_cc[0], "credit_in_account_currency": flt(amount, precision), - "cost_center": self.cost_center, + "cost_center": acc_cc[1] or self.cost_center, "party_type": '', "project": self.project }) @@ -253,6 +259,7 @@ class PayrollEntry(Document): "account": default_payroll_payable_account, "credit_in_account_currency": flt(payable_amount, precision), "party_type": '', + "cost_center": self.cost_center }) journal_entry.set("accounts", accounts) diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py index e43f744bd43..3c318e78a24 100644 --- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py @@ -10,15 +10,16 @@ from frappe.utils import add_months from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ - make_earning_salary_component, make_deduction_salary_component + make_earning_salary_component, make_deduction_salary_component, create_account from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans class TestPayrollEntry(unittest.TestCase): def setUp(self): - for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Salary Structure"]: - frappe.db.sql("delete from `tab%s`" % dt) + for dt in ["Salary Slip", "Salary Component", "Salary Component Account", + "Payroll Entry", "Salary Structure", "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary"]: + frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) @@ -33,11 +34,59 @@ class TestPayrollEntry(unittest.TestCase): get_salary_component_account(data.name) employee = frappe.db.get_value("Employee", {'company': company}) - make_salary_structure("_Test Salary Structure", "Monthly", employee) + make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date) + def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use + for data in frappe.get_all('Salary Component', fields = ["name"]): + if not frappe.db.get_value('Salary Component Account', + {'parent': data.name, 'company': "_Test Company"}, 'name'): + get_salary_component_account(data.name) + + if not frappe.db.exists('Department', "cc - _TC"): + frappe.get_doc({ + 'doctype': 'Department', + 'department_name': "cc", + "company": "_Test Company" + }).insert() + + employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", + department="cc - _TC", company="_Test Company") + employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", + department="cc - _TC", company="_Test Company") + + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company") + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company") + + if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): + create_account(account_name="_Test Payroll Payable", + company="_Test Company", parent_account="Current Liabilities - _TC") + frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", + "_Test Payroll Payable - _TC") + + dates = get_start_end_dates('Monthly', nowdate()) + if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): + pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, + department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") + je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") + je_entries = frappe.db.sql(""" + select account, cost_center, debit, credit + from `tabJournal Entry Account` + where parent=%s + order by account, cost_center + """, je) + expected_je = ( + ('_Test Payroll Payable - _TC', 'Main - _TC', 0.0, 155600.0), + ('Salary - _TC', '_Test Cost Center - _TC', 78000.0, 0.0), + ('Salary - _TC', '_Test Cost Center 2 - _TC', 78000.0, 0.0), + ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 200.0), + ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 200.0) + ) + + self.assertEqual(je_entries, expected_je) + def test_get_end_date(self): self.assertEqual(get_end_date('2017-01-01', 'monthly'), {'end_date': '2017-01-31'}) self.assertEqual(get_end_date('2017-02-01', 'monthly'), {'end_date': '2017-02-28'}) @@ -49,7 +98,6 @@ class TestPayrollEntry(unittest.TestCase): self.assertEqual(get_end_date('2017-02-15', 'daily'), {'end_date': '2017-02-15'}) def test_loan(self): - branch = "Test Employee Branch" applicant = make_employee("test_employee@loan.com", company="_Test Company") company = "_Test Company" @@ -116,6 +164,7 @@ def make_payroll_entry(**args): payroll_entry.posting_date = nowdate() payroll_entry.payroll_frequency = "Monthly" payroll_entry.branch = args.branch or None + payroll_entry.department = args.department or None if args.cost_center: payroll_entry.cost_center = args.cost_center @@ -123,6 +172,7 @@ def make_payroll_entry(**args): if args.payment_account: payroll_entry.payment_account = args.payment_account + payroll_entry.fill_employee_details() payroll_entry.save() payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.json b/erpnext/hr/doctype/salary_slip/salary_slip.json index 54a8164587e..cfd4d897314 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.json +++ b/erpnext/hr/doctype/salary_slip/salary_slip.json @@ -12,6 +12,7 @@ "department", "designation", "branch", + "payroll_cost_center", "column_break1", "status", "journal_entry", @@ -459,13 +460,22 @@ "options": "Salary Slip", "print_hide": 1, "read_only": 1 + }, + { + "fetch_from": "employee.payroll_cost_center", + "fetch_if_empty": 1, + "fieldname": "payroll_cost_center", + "fieldtype": "Link", + "label": "Payroll Cost Center", + "options": "Cost Center", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-04-14 20:02:53.159827", + "modified": "2020-05-05 18:55:26.173629", "modified_by": "Administrator", "module": "HR", "name": "Salary Slip", diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index a7dcb941677..3eff738ec8d 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -422,22 +422,32 @@ def get_salary_component_account(sal_comp, company_list=None): sal_comp = frappe.get_doc("Salary Component", sal_comp) if not sal_comp.get("accounts"): for d in company_list: + company_abbr = frappe.get_cached_value('Company', d, 'abbr') + + if sal_comp.type == "Earning": + account_name = "Salary" + parent_account = "Indirect Expenses - " + company_abbr + else: + account_name = "Salary Deductions" + parent_account = "Current Liabilities - " + company_abbr + sal_comp.append("accounts", { "company": d, - "default_account": create_account(d) + "default_account": create_account(account_name, d, parent_account) }) sal_comp.save() -def create_account(company): - salary_account = frappe.db.get_value("Account", "Salary - " + frappe.get_cached_value('Company', company, 'abbr')) - if not salary_account: +def create_account(account_name, company, parent_account): + company_abbr = frappe.get_cached_value('Company', company, 'abbr') + account = frappe.db.get_value("Account", account_name + " - " + company_abbr) + if not account: frappe.get_doc({ "doctype": "Account", - "account_name": "Salary", - "parent_account": "Indirect Expenses - " + frappe.get_cached_value('Company', company, 'abbr'), + "account_name": account_name, + "parent_account": parent_account, "company": company }).insert() - return salary_account + return account def make_earning_salary_component(setup=False, test_tax=False, company_list=None): data = [ @@ -683,7 +693,7 @@ def setup_test(): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]: + for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance", "Additional Salary"]: frappe.db.sql("delete from `tab%s`" % dt) make_holiday_list() diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index 5ba7f1c4327..ffc16d73c25 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -153,12 +153,16 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = def postprocess(source, target): if employee: employee_details = frappe.db.get_value("Employee", employee, - ["employee_name", "branch", "designation", "department"], as_dict=1) + ["employee_name", "branch", "designation", "department", "payroll_cost_center"], as_dict=1) target.employee = employee target.employee_name = employee_details.employee_name target.branch = employee_details.branch target.designation = employee_details.designation target.department = employee_details.department + target.payroll_cost_center = employee_details.payroll_cost_center + if not target.payroll_cost_center and target.department: + target.payroll_cost_center = frappe.db.get_value("Department", target.department, "payroll_cost_center") + target.run_method('process_salary_structure', for_preview=for_preview) doc = get_mapped_doc("Salary Structure", source_name, { diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py index c1869f05d7d..eb5311e3b81 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py @@ -128,6 +128,7 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do salary_structure_doc.insert() if not dont_submit: salary_structure_doc.submit() + else: salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py new file mode 100644 index 00000000000..ef61f230acd --- /dev/null +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -0,0 +1,240 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe, erpnext, json +from frappe import _ +from frappe.utils import nowdate, get_date_str +from erpnext.accounts.utils import get_fiscal_year + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + }) + +def get_dashboards(): + return [{ + "name": "Manufacturing", + "dashboard_name": "Manufacturing", + "charts": [ + { "chart": "Produced Quantity", "width": "Half" }, + { "chart": "Completed Operation", "width": "Half" }, + { "chart": "Work Order Analysis", "width": "Half" }, + { "chart": "Quality Inspection Analysis", "width": "Half" }, + { "chart": "Pending Work Order", "width": "Half" }, + { "chart": "Last Month Downtime Analysis", "width": "Half" }, + { "chart": "Work Order Qty Analysis", "width": "Full" }, + { "chart": "Job Card Analysis", "width": "Full" } + ], + "cards": [ + { "card": "Total Work Order" }, + { "card": "Completed Work Order" }, + { "card": "Ongoing Job Card" }, + { "card": "Total Quality Inspection"} + ] + }] + +def get_charts(): + company = erpnext.get_default_company() + + if not company: + company = frappe.db.get_value("Company", {"is_group": 0}, "name") + + return [{ + "doctype": "Dashboard Chart", + "based_on": "modified", + "time_interval": "Yearly", + "chart_type": "Sum", + "chart_name": _("Produced Quantity"), + "name": "Produced Quantity", + "document_type": "Work Order", + "filters_json": json.dumps([['Work Order', 'docstatus', '=', 1, False]]), + "group_by_type": "Count", + "time_interval": "Monthly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "produced_qty", + "is_public": 1, + "timeseries": 1 + }, { + "doctype": "Dashboard Chart", + "based_on": "creation", + "time_interval": "Yearly", + "chart_type": "Sum", + "chart_name": _("Completed Operation"), + "name": "Completed Operation", + "document_type": "Work Order Operation", + "filters_json": json.dumps([['Work Order Operation', 'docstatus', '=', 1, False]]), + "group_by_type": "Count", + "time_interval": "Quarterly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "completed_qty", + "is_public": 1, + "timeseries": 1 + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Analysis"), + "name": "Work Order Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "owner": "Administrator", + "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "height": 300 + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Quality Inspection Analysis"), + "name": "Quality Inspection Analysis", + "timespan": "Last Year", + "report_name": "Quality Inspection Summary", + "owner": "Administrator", + "filters_json": json.dumps({}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "height": 300 + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Pending Work Order"), + "name": "Pending Work Order", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}), + "owner": "Administrator", + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "height": 300 + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Last Month Downtime Analysis"), + "name": "Last Month Downtime Analysis", + "timespan": "Last Year", + "filters_json": json.dumps({}), + "report_name": "Downtime Analysis", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "type": "Bar" + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Qty Analysis"), + "name": "Work Order Qty Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}), + "owner": "Administrator", + "type": "Bar", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "barOptions": { "stacked": 1 } + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Job Card Analysis"), + "name": "Job Card Analysis", + "timespan": "Last Year", + "report_name": "Job Card Summary", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "filters_json": json.dumps({"company": company, "docstatus": 1, "range":"Monthly"}), + "custom_options": json.dumps({ + "barOptions": { "stacked": 1 } + }), + "type": "Bar" + }] + +def get_number_cards(): + fiscal_year = get_fiscal_year(date=nowdate()) + year_start_date = get_date_str(fiscal_year[1]) + year_end_date = get_date_str(fiscal_year[2]) + + return [{ + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Total Work Order", + "filters_json": json.dumps([ + ['Work Order', 'docstatus', '=', 1], + ['Work Order', 'creation', 'between', [year_start_date, year_end_date]] + ]), + "function": "Count", + "is_public": 1, + "label": _("Total Work Order"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Completed Work Order", + "filters_json": json.dumps([ + ['Work Order', 'status', '=', 'Completed'], + ['Work Order', 'docstatus', '=', 1], + ['Work Order', 'creation', 'between', [year_start_date, year_end_date]] + ]), + "function": "Count", + "is_public": 1, + "label": _("Completed Work Order"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Job Card", + "name": "Ongoing Job Card", + "filters_json": json.dumps([ + ['Job Card', 'status','!=','Completed'], + ['Job Card', 'docstatus', '=', 1] + ]), + "function": "Count", + "is_public": 1, + "label": _("Ongoing Job Card"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Quality Inspection", + "name": "Total Quality Inspection", + "filters_json": json.dumps([['Quality Inspection', 'docstatus', '=', 1]]), + "function": "Count", + "is_public": 1, + "label": _("Total Quality Inspection"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }] \ No newline at end of file diff --git a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json index 18604e283ab..e35f1fb4ead 100644 --- a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Production", - "links": "[\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Orders released for production.\",\n \"label\": \"Work Order\",\n \"name\": \"Work Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Generate Material Requests (MRP) and Work Orders.\",\n \"label\": \"Production Plan\",\n \"name\": \"Production Plan\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Activity Type\"\n ],\n \"description\": \"Time Sheet for manufacturing.\",\n \"label\": \"Timesheet\",\n \"name\": \"Timesheet\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Card\",\n \"name\": \"Job Card\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Orders released for production.\",\n \"label\": \"Work Order\",\n \"name\": \"Work Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Generate Material Requests (MRP) and Work Orders.\",\n \"label\": \"Production Plan\",\n \"name\": \"Production Plan\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Card\",\n \"name\": \"Job Card\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Downtime Entry\",\n \"name\": \"Downtime Entry\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Open Work Orders\",\n \"name\": \"Open Work Orders\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Work Orders in Progress\",\n \"name\": \"Work Orders in Progress\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Issued Items Against Work Order\",\n \"name\": \"Issued Items Against Work Order\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Completed Work Orders\",\n \"name\": \"Completed Work Orders\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Production Analytics\",\n \"name\": \"Production Analytics\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Search\",\n \"name\": \"BOM Search\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Stock Report\",\n \"name\": \"BOM Stock Report\",\n \"type\": \"report\"\n }\n]" + "links": "[{\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Work Order Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Work Order Summary\"\n}, {\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Production Analytics\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Production Analytics\"\n}, {\n\t\"dependencies\": [\"Quality Inspection\"],\n\t\"name\": \"Quality Inspection Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Quality Inspection\",\n\t\"label\": \"Quality Inspection Summary\"\n}, {\n\t\"dependencies\": [\"Downtime Entry\"],\n\t\"name\": \"Downtime Analysis\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Downtime Entry\",\n\t\"label\": \"Downtime Analysis\"\n}, {\n\t\"dependencies\": [\"Job Card\"],\n\t\"name\": \"Job Card Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Job Card\",\n\t\"label\": \"Job Card Summary\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Search\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Search\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Stock Report\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Stock Report\"\n}]" }, { "hidden": 0, @@ -32,7 +32,11 @@ } ], "category": "Domains", - "charts": [], + "charts": [ + { + "chart_name": "Produced Quantity" + } + ], "creation": "2020-03-02 17:11:37.032604", "developer_mode_only": 0, "disable_user_customization": 0, @@ -42,13 +46,59 @@ "idx": 0, "is_standard": 1, "label": "Manufacturing", - "modified": "2020-04-01 11:28:50.979358", + "modified": "2020-05-19 12:54:04.104444", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", + "onboarding": "Manufacturing", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Manufacturing", - "shortcuts": [] + "shortcuts": [ + { + "format": "{} Active", + "label": "Item", + "link_to": "Item", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{\n \"disabled\": 0\n}", + "type": "DocType" + }, + { + "format": "{} Active", + "label": "BOM", + "link_to": "BOM", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{\n \"is_active\": 1\n}", + "type": "DocType" + }, + { + "format": "{} Open", + "label": "Work Order", + "link_to": "Work Order", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}", + "type": "DocType" + }, + { + "format": "{} Open", + "label": "Production Plan", + "link_to": "Production Plan", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}", + "type": "DocType" + }, + { + "label": "Work Order Summary", + "link_to": "Work Order Summary", + "restrict_to_domain": "Manufacturing", + "type": "Report" + }, + { + "label": "Manufacturing Dashboard", + "link_to": "Manufacturing Dashboard", + "restrict_to_domain": "Manufacturing", + "type": "Dashboard" + } + ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ebfb7626407..44da9cae355 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -212,6 +212,12 @@ frappe.ui.form.on("BOM", { }); }, + rm_cost_as_per: function(frm) { + if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { + frm.set_value("plc_conversion_rate", 1.0); + } + }, + routing: function(frm) { if (frm.doc.routing) { frappe.call({ @@ -242,7 +248,7 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ item_code: function(doc, cdt, cdn){ var scrap_items = false; var child = locals[cdt][cdn]; - if(child.doctype == 'BOM Scrap Item') { + if (child.doctype == 'BOM Scrap Item') { scrap_items = true; } @@ -252,8 +258,19 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ get_bom_material_detail(doc, cdt, cdn, scrap_items); }, + + buying_price_list: function(doc) { + this.apply_price_list(); + }, + + plc_conversion_rate: function(doc) { + if (!this.in_apply_price_list) { + this.apply_price_list(); + } + }, + conversion_factor: function(doc, cdt, cdn) { - if(frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 63f4f977c59..4ce0ecf3f27 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2013-01-22 15:11:38", "doctype": "DocType", @@ -6,23 +7,25 @@ "engine": "InnoDB", "field_order": [ "item", - "quantity", - "set_rate_of_sub_assembly_item_based_on_bom", + "company", + "item_name", + "uom", "cb0", "is_active", "is_default", "allow_alternative_item", - "image", - "item_name", - "uom", - "currency_detail", - "company", + "set_rate_of_sub_assembly_item_based_on_bom", "project", + "quantity", + "image", + "currency_detail", + "currency", "conversion_rate", "column_break_12", - "currency", "rm_cost_as_per", "buying_price_list", + "price_list_currency", + "plc_conversion_rate", "section_break_21", "with_operations", "column_break_23", @@ -176,7 +179,8 @@ }, { "fieldname": "currency_detail", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Currency and Price List" }, { "fieldname": "company", @@ -324,7 +328,7 @@ }, { "fieldname": "base_scrap_material_cost", - "fieldtype": "Data", + "fieldtype": "Currency", "label": "Scrap Material Cost(Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -477,13 +481,31 @@ { "fieldname": "column_break_52", "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-sitemap", "idx": 1, "image_field": "image", "is_submittable": 1, - "modified": "2019-11-22 14:35:12.142150", + "links": [], + "modified": "2020-05-05 14:29:32.634952", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b1fc4deae91..6ac653e37a4 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -70,11 +70,13 @@ class BOM(WebsiteGenerator): self.validate_main_item() self.validate_currency() self.set_conversion_rate() + self.set_plc_conversion_rate() self.validate_uom_is_interger() self.set_bom_material_details() self.validate_materials() self.validate_operations() self.calculate_cost() + self.update_cost(update_parent=False, from_child_bom=True, save=False) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -165,7 +167,7 @@ class BOM(WebsiteGenerator): 'rate' : rate, 'qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, - 'base_rate' : rate, + 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0 } @@ -226,7 +228,7 @@ class BOM(WebsiteGenerator): frappe.msgprint(_("{0} not found for item {1}") .format(self.rm_cost_as_per, arg["item_code"]), alert=True) - return flt(rate) / (self.conversion_rate or 1) + return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) def update_cost(self, update_parent=True, from_child_bom=False, save=True): if self.docstatus == 2: @@ -243,10 +245,15 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor }) + if rate: d.rate = rate d.amount = flt(d.rate) * flt(d.qty) - d.db_update() + d.base_rate = flt(d.rate) * flt(self.conversion_rate) + d.base_amount = flt(d.amount) * flt(self.conversion_rate) + + if save: + d.db_update() if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True @@ -372,6 +379,13 @@ class BOM(WebsiteGenerator): elif self.conversion_rate == 1 or flt(self.conversion_rate) <= 0: self.conversion_rate = get_exchange_rate(self.currency, self.company_currency(), args="for_buying") + def set_plc_conversion_rate(self): + if self.rm_cost_as_per in ["Valuation Rate", "Last Purchase Rate"]: + self.plc_conversion_rate = 1 + elif not self.plc_conversion_rate and self.price_list_currency: + self.plc_conversion_rate = get_exchange_rate(self.price_list_currency, + self.company_currency(), args="for_buying") + def validate_materials(self): """ Validate raw material entries """ diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 45a7b935d38..3dfd03b1395 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -81,13 +81,13 @@ class TestBOM(unittest.TestCase): # test amounts in selected currency self.assertEqual(bom.operating_cost, 100) - self.assertEqual(bom.raw_material_cost, 8000) - self.assertEqual(bom.total_cost, 8100) + self.assertEqual(bom.raw_material_cost, 351.68) + self.assertEqual(bom.total_cost, 451.68) # test amounts in selected currency self.assertEqual(bom.base_operating_cost, 6000) - self.assertEqual(bom.base_raw_material_cost, 480000) - self.assertEqual(bom.base_total_cost, 486000) + self.assertEqual(bom.base_raw_material_cost, 21100.80) + self.assertEqual(bom.base_total_cost, 27100.80) def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) diff --git a/erpnext/manufacturing/doctype/downtime_entry/__init__.py b/erpnext/manufacturing/doctype/downtime_entry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js new file mode 100644 index 00000000000..3b7f5ba8d7f --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Downtime Entry', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json new file mode 100644 index 00000000000..9acb4f05133 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json @@ -0,0 +1,132 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2020-04-18 04:50:46.187638", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "workstation", + "operator", + "column_break_4", + "from_time", + "to_time", + "downtime", + "downtime_reason_section", + "stop_reason", + "column_break_9", + "remarks" + ], + "fields": [ + { + "fieldname": "workstation", + "fieldtype": "Link", + "label": "Workstation / Machine", + "options": "Workstation", + "reqd": 1 + }, + { + "fieldname": "from_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "fieldname": "to_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "operator", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operator", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "downtime_reason_section", + "fieldtype": "Section Break", + "label": "Downtime Reason" + }, + { + "description": "In Mins", + "fieldname": "downtime", + "fieldtype": "Float", + "label": "Downtime", + "read_only": 1 + }, + { + "fieldname": "stop_reason", + "fieldtype": "Select", + "label": "Stop Reason", + "options": "\nExcessive machine set up time\nUnplanned machine maintenance\nOn-machine press checks\nMachine operator errors\nMachine malfunction\nElectricity down\nOther", + "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "remarks", + "fieldtype": "Text", + "label": "Remarks" + } + ], + "links": [], + "modified": "2020-05-19 12:59:37.358483", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Downtime Entry", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "workstation", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py new file mode 100644 index 00000000000..56ec4356af4 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import time_diff_in_hours +from frappe.model.document import Document + +class DowntimeEntry(Document): + def validate(self): + if self.from_time and self.to_time: + self.downtime = time_diff_in_hours(self.to_time, self.from_time) * 60 diff --git a/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py b/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py new file mode 100644 index 00000000000..8b2a8d36c12 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestDowntimeEntry(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 7661fffa864..fba670c1c15 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2018-07-09 17:23:29.518745", "doctype": "DocType", @@ -264,8 +265,10 @@ { "fetch_from": "work_order.production_item", "fieldname": "production_item", - "fieldtype": "Read Only", - "label": "Production Item" + "fieldtype": "Link", + "label": "Production Item", + "options": "Item", + "read_only": 1 }, { "fieldname": "barcode", @@ -274,7 +277,8 @@ "read_only": 1 }, { - "fetch_from": "work_order.item_name", + "fetch_from": "production_item.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Read Only", "label": "Item Name" @@ -290,7 +294,8 @@ } ], "is_submittable": 1, - "modified": "2020-03-27 13:36:35.417502", + "links": [], + "modified": "2020-04-20 15:14:00.273441", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 00a67a03d67..585a09db2bf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -38,11 +38,12 @@ "required_items", "time", "planned_start_date", - "actual_start_date", - "column_break_13", "planned_end_date", - "actual_end_date", "expected_delivery_date", + "column_break_13", + "actual_start_date", + "actual_end_date", + "lead_time", "operations_section", "transfer_material_against", "operations", @@ -108,6 +109,8 @@ }, { "depends_on": "eval:doc.production_item", + "fetch_from": "production_item.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", @@ -281,27 +284,30 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "actual_start_date", "fieldtype": "Datetime", "label": "Actual Start Date", - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { "fieldname": "column_break_13", "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "planned_end_date", "fieldtype": "Datetime", "label": "Planned End Date", "no_copy": 1, - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { + "allow_on_submit": 1, "fieldname": "actual_end_date", "fieldtype": "Datetime", "label": "Actual End Date", - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { "allow_on_submit": 1, @@ -476,6 +482,13 @@ "fieldtype": "Link", "label": "Source Warehouse", "options": "Warehouse" + }, + { + "description": "In Mins", + "fieldname": "lead_time", + "fieldtype": "Float", + "label": "Lead Time", + "read_only": 1 } ], "icon": "fa fa-cogs", @@ -483,7 +496,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-04-24 19:32:43.323054", + "modified": "2020-05-05 19:32:43.323054", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8301f30d837..c2789559b07 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -6,7 +6,7 @@ import frappe import json import math from frappe import _ -from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form +from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form, time_diff_in_hours from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict from dateutil.relativedelta import relativedelta @@ -279,7 +279,7 @@ class WorkOrder(Document): if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time row.planned_end_time = job_card_doc.time_logs[-1].to_time - print(row.planned_start_time, original_start_time, plan_days) + if date_diff(row.planned_start_time, original_start_time) > plan_days: frappe.message_log.pop() frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") @@ -437,8 +437,6 @@ class WorkOrder(Document): frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) def set_actual_dates(self): - self.actual_start_date = None - self.actual_end_date = None if self.get("operations"): actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] if actual_start_dates: @@ -447,6 +445,27 @@ class WorkOrder(Document): actual_end_dates = [d.actual_end_time for d in self.get("operations") if d.actual_end_time] if actual_end_dates: self.actual_end_date = max(actual_end_dates) + else: + data = frappe.get_all("Stock Entry", + fields = ["timestamp(posting_date, posting_time) as posting_datetime"], + filters = { + "work_order": self.name, + "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]) + } + ) + + if data and len(data): + dates = [d.posting_datetime for d in data] + self.actual_start_date = min(dates) + + if self.status == "Completed": + self.actual_end_date = max(dates) + + self.set_lead_time() + + def set_lead_time(self): + if self.actual_start_date and self.actual_end_date: + self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60) def delete_job_card(self): for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}): diff --git a/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json new file mode 100644 index 00000000000..952d1f0e071 --- /dev/null +++ b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json @@ -0,0 +1,57 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-05 16:37:08.238935", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-19 12:51:42.744570", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing", + "owner": "Administrator", + "steps": [ + { + "step": "Warehouse" + }, + { + "step": "Workstation" + }, + { + "step": "Operation" + }, + { + "step": "Create Product" + }, + { + "step": "Create Raw Materials" + }, + { + "step": "Create BOM" + }, + { + "step": "Work Order" + }, + { + "step": "Explore Manufacturing Settings" + } + ], + "subtitle": "Products, Raw Materials, BOM, Work Order and more.", + "success_message": "Manufacturing module is all setup!", + "title": "Let's Setup Manufacturing Module", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json new file mode 100644 index 00000000000..50584e19cac --- /dev/null +++ b/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json @@ -0,0 +1,54 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-05 16:37:08.238935", + "docstatus": 0, + "doctype": "Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-12 16:22:07.050224", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to Manufacturing" + }, + { + "step": "Warehouse" + }, + { + "step": "Workstation" + }, + { + "step": "Operation" + }, + { + "step": "Create Product" + }, + { + "step": "Create BOM" + }, + { + "step": "Work Order" + } + ], + "subtitle": "Products, Raw Materials, BOM, Work Order and more.", + "success_message": "Manufacturing module is all setup!", + "title": "Let's Setup Manufacturing Module", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json b/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json new file mode 100644 index 00000000000..84b4088f233 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:41:20.239696", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:51:31.315686", + "modified_by": "Administrator", + "name": "Create BOM", + "owner": "Administrator", + "reference_document": "BOM", + "show_full_form": 1, + "title": "Create a BOM (Bill of Material)", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_product/create_product.json b/erpnext/manufacturing/onboarding_step/create_product/create_product.json new file mode 100644 index 00000000000..0ffa30158b0 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_product/create_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:42:31.476275", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:59.010439", + "modified_by": "Administrator", + "name": "Create Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Finished Good", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_raw_materials/create_raw_materials.json b/erpnext/manufacturing/onboarding_step/create_raw_materials/create_raw_materials.json new file mode 100644 index 00000000000..0764f2e44a8 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_raw_materials/create_raw_materials.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-19 11:53:17.295372", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 11:53:25.147837", + "modified_by": "Administrator", + "name": "Create Raw Materials", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create Raw Materials", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/explore_manufacturing_settings/explore_manufacturing_settings.json b/erpnext/manufacturing/onboarding_step/explore_manufacturing_settings/explore_manufacturing_settings.json new file mode 100644 index 00000000000..582aba40d68 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/explore_manufacturing_settings/explore_manufacturing_settings.json @@ -0,0 +1,20 @@ +{ + "action": "Update Settings", + "creation": "2020-05-19 11:55:11.378374", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-19 12:12:28.145366", + "modified_by": "Administrator", + "name": "Explore Manufacturing Settings", + "owner": "Administrator", + "reference_document": "Manufacturing Settings", + "show_full_form": 0, + "title": "Explore Manufacturing Settings", + "validate_action": 0, + "video_url": "https://www.youtube.com/watch?v=UVGfzwOOZC4" +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json b/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json new file mode 100644 index 00000000000..eb7ab3a175c --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json @@ -0,0 +1,20 @@ +{ + "action": "Update Settings", + "creation": "2020-05-05 16:40:23.676406", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:11:57.152883", + "modified_by": "Administrator", + "name": "Introduction to Manufacturing", + "owner": "Administrator", + "reference_document": "Manufacturing Settings", + "show_full_form": 0, + "title": "Manufacturing Settings", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=UVGfzwOOZC4" +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/operation/operation.json b/erpnext/manufacturing/onboarding_step/operation/operation.json new file mode 100644 index 00000000000..b532e6778c4 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/operation/operation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:15:31.706756", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:41.642754", + "modified_by": "Administrator", + "name": "Operation", + "owner": "Administrator", + "reference_document": "Operation", + "show_full_form": 0, + "title": "Create a Operation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json b/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json new file mode 100644 index 00000000000..e23bd33b787 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:13:34.014554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:13.766712", + "modified_by": "Administrator", + "name": "Warehouse", + "owner": "Administrator", + "reference_document": "Warehouse", + "show_full_form": 0, + "title": "Create a Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/work_order/work_order.json b/erpnext/manufacturing/onboarding_step/work_order/work_order.json new file mode 100644 index 00000000000..c63363e7cb2 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/work_order/work_order.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:15:56.084682", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:51:38.133150", + "modified_by": "Administrator", + "name": "Work Order", + "owner": "Administrator", + "reference_document": "Work Order", + "show_full_form": 1, + "title": "Create a Work Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/workstation/workstation.json b/erpnext/manufacturing/onboarding_step/workstation/workstation.json new file mode 100644 index 00000000000..df244bb494a --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/workstation/workstation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:14:14.930214", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:33.938176", + "modified_by": "Administrator", + "name": "Workstation", + "owner": "Administrator", + "reference_document": "Workstation", + "show_full_form": 0, + "title": "Create a Workstation / Machine", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/downtime_analysis/__init__.py b/erpnext/manufacturing/report/downtime_analysis/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js new file mode 100644 index 00000000000..ff32dbed98a --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js @@ -0,0 +1,28 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Downtime Analysis"] = { + "filters": [ + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Datetime", + default: frappe.datetime.add_months(frappe.datetime.now_datetime(), -1), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + reqd: 1, + }, + { + label: __("Machine"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + } + ] +}; diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json new file mode 100644 index 00000000000..5edc7781a2e --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 1, + "creation": "2020-04-20 18:26:04.345289", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-20 18:26:04.345289", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Downtime Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Downtime Entry", + "report_name": "Downtime Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py new file mode 100644 index 00000000000..2b2be4faa3b --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py @@ -0,0 +1,108 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {} + + fields = ["name", "workstation", "operator", "from_time", "to_time", "downtime", "stop_reason", "remarks"] + + query_filters["from_time"] = (">=", filters.get("from_date")) + query_filters["to_time"] = ("<=", filters.get("to_date")) + + if filters.get("workstation"): + query_filters["workstation"] = filters.get("workstation") + + return frappe.get_all("Downtime Entry", fields= fields, filters=query_filters) + +def get_chart_data(data, columns): + labels = sorted(list(set([d.workstation for d in data]))) + + workstation_wise_data = {} + for d in data: + if d.workstation not in workstation_wise_data: + workstation_wise_data[d.workstation] = 0 + + workstation_wise_data[d.workstation] += flt(d.downtime, 2) + + datasets = [] + for label in labels: + datasets.append(workstation_wise_data.get(label, 0)) + + chart = { + "data": { + "labels": labels, + "datasets": [ + {"name": "Dataset 1", "values": datasets} + ] + }, + "type": "bar" + } + + return chart + +def get_columns(filters): + return [ + { + "label": _("ID"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Downtime Entry", + "width": 100 + }, + { + "label": _("Machine"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 100 + }, + { + "label": _("Operator"), + "fieldname": "operator", + "fieldtype": "Link", + "options": "Employee", + "width": 130 + }, + { + "label": _("From Time"), + "fieldname": "from_time", + "fieldtype": "Datetime", + "width": 160 + }, + { + "label": _("To Time"), + "fieldname": "to_time", + "fieldtype": "Datetime", + "width": 160 + }, + { + "label": _("Downtime (In Mins)"), + "fieldname": "downtime", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Stop Reason"), + "fieldname": "stop_reason", + "fieldtype": "Data", + "width": 220 + }, + { + "label": _("Remarks"), + "fieldname": "remarks", + "fieldtype": "Text", + "width": 100 + } + ] \ No newline at end of file diff --git a/erpnext/manufacturing/report/job_card_summary/__init__.py b/erpnext/manufacturing/report/job_card_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js new file mode 100644 index 00000000000..b7e307183f8 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -0,0 +1,52 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Job Card Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Open", "Work In Progress", "Completed", "On Hold"] + }, + { + label: __("Sales Orders"), + fieldname: "sales_order", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Sales Order', txt); + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Item', txt); + } + } + ] +}; diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json new file mode 100644 index 00000000000..9f08fc34cb8 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json @@ -0,0 +1,34 @@ +{ + "add_total_row": 0, + "creation": "2020-04-20 12:00:21.436619", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-20 12:00:21.436619", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Job Card", + "report_name": "Job Card Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py new file mode 100644 index 00000000000..ae1e4f30469 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -0,0 +1,186 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import getdate, flt +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {"docstatus": ("<", 2)} + + fields = ["name", "status", "work_order", "production_item", "item_name", + "total_completed_qty", "workstation", "operation", "employee_name", "total_time_in_mins"] + + for field in ["work_order", "workstation", "operation", "company"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + data = frappe.get_all("Job Card", + fields= fields, filters=query_filters) + + if not data: return [] + + job_cards = [d.name for d in data] + job_card_time_details = {} + for job_card_data in frappe.get_all("Job Card Time Log", + fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"], + filters={"docstatus": ("<", 2), "parent": ("in", job_cards)}, group_by="parent"): + job_card_time_details[job_card_data.parent] = job_card_data + + for d in data: + if d.status == "Material Transferred": + d.status = "Open" + + if job_card_time_details.get(d.name): + d.from_time = job_card_time_details.get(d.name).from_time + d.to_time = job_card_time_details.get(d.name).to_time + + return data + +def get_chart_data(job_card_details, filters): + labels, periodic_data = prepare_chart_data(job_card_details, filters) + + pending, completed = [], [] + datasets = [] + + for d in labels: + pending.append(periodic_data.get("Pending").get(d)) + completed.append(periodic_data.get("Completed").get(d)) + + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "bar" + } + + return chart + +def prepare_chart_data(job_card_details, filters): + labels = [] + + periodic_data = { + "Pending": {}, + "Completed": {} + } + + filters.range = "Monthly" + + ranges = get_period_date_ranges(filters) + for from_date, end_date in ranges: + period = get_period(end_date, filters) + if period not in labels: + labels.append(period) + + for d in job_card_details: + if getdate(d.from_time) >= from_date and getdate(d.to_time) <= end_date: + status = "Completed" if d.status == "Completed" else "Pending" + + if periodic_data.get(status) and periodic_data.get(status).get(period): + periodic_data[status][period] += 1 + else: + periodic_data[status][period] = 1 + + return labels, periodic_data + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Job Card", + "width": 100 + }, + ] + + if not filters.get("status"): + columns.append( + { + "label": _("Status"), + "fieldname": "status", + "width": 100 + }, + ) + + columns.extend([ + { + "label": _("Work Order"), + "fieldname": "work_order", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 110 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 100 + }, + { + "label": _("Workstation"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 110 + }, + { + "label": _("Operation"), + "fieldname": "operation", + "fieldtype": "Link", + "options": "Operation", + "width": 110 + }, + { + "label": _("Employee Name"), + "fieldname": "employee_name", + "fieldtype": "Data", + "width": 110 + }, + { + "label": _("Total Completed Qty"), + "fieldname": "total_completed_qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("From Time"), + "fieldname": "from_time", + "fieldtype": "Datetime", + "width": 120 + }, + { + "label": _("To Time"), + "fieldname": "to_time", + "fieldtype": "Datetime", + "width": 120 + }, + { + "label": _("Time Required (In Mins)"), + "fieldname": "total_time_in_mins", + "fieldtype": "Float", + "width": 100 + } + ]) + + return columns \ No newline at end of file diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 7447a1f6705..f62cd255b5e 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -61,7 +61,7 @@ def get_periodic_data(filters, entry): elif getdate(d.planned_start_date) < getdate(from_date) : periodic_data = update_periodic_data(periodic_data, "Overdue", period) - + else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) @@ -148,4 +148,3 @@ def get_chart_data(periodic_data, columns): - diff --git a/erpnext/manufacturing/report/quality_inspection_summary/__init__.py b/erpnext/manufacturing/report/quality_inspection_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js new file mode 100644 index 00000000000..d4587aa6619 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js @@ -0,0 +1,40 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Quality Inspection Summary"] = { + "filters": [ + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Accepted", "Rejected"] + }, + { + label: __("Item Code"), + fieldname: "item_code", + fieldtype: "Link", + options: "Item" + }, + { + label: __("Inspected By"), + fieldname: "inspected_by", + fieldtype: "Link", + options: "User" + } + ] +}; diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json new file mode 100644 index 00000000000..48226e6b21d --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "creation": "2020-04-26 18:23:53.475110", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "Gadgets International", + "modified": "2020-04-26 18:24:50.529940", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Quality Inspection Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Quality Inspection", + "report_name": "Quality Inspection Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Quality Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py new file mode 100644 index 00000000000..6192632bda6 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data , None, chart_data + +def get_data(filters): + query_filters = {"docstatus": ("<", 2)} + + fields = ["name", "status", "report_date", "item_code", "item_name", "sample_size", + "inspection_type", "reference_type", "reference_name", "inspected_by"] + + for field in ["status", "item_code", "status", "inspected_by"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + query_filters["report_date"] = (">=", filters.get("from_date")) + query_filters["report_date"] = ("<=", filters.get("to_date")) + + return frappe.get_all("Quality Inspection", + fields= fields, filters=query_filters, order_by="report_date asc") + +def get_chart_data(periodic_data, columns): + labels = ["Rejected", "Accepted"] + + status_wise_data = { + "Accepted": 0, + "Rejected": 0 + } + + datasets = [] + + for d in periodic_data: + status_wise_data[d.status] += 1 + + datasets.append({'name':'Qty Wise Chart', + 'values': [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")]}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + { + "label": _("Report Date"), + "fieldname": "report_date", + "fieldtype": "Date", + "width": 150 + } + ] + + if not filters.get("status"): + columns.append( + { + "label": _("Status"), + "fieldname": "status", + "width": 100 + }, + ) + + columns.extend([ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 130 + }, + { + "label": _("Sample Size"), + "fieldname": "sample_size", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Inspection Type"), + "fieldname": "inspection_type", + "fieldtype": "Data", + "width": 110 + }, + { + "label": _("Document Type"), + "fieldname": "reference_type", + "fieldtype": "Data", + "width": 90 + }, + { + "label": _("Document Name"), + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "options": "reference_type", + "width": 150 + }, + { + "label": _("Inspected By"), + "fieldname": "inspected_by", + "fieldtype": "Link", + "options": "User", + "width": 150 + } + ]) + + return columns \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_summary/__init__.py b/erpnext/manufacturing/report/work_order_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js new file mode 100644 index 00000000000..ec9fe35d63a --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -0,0 +1,65 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Work Order Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Not Started", "In Process", "Completed", "Stopped"] + }, + { + label: __("Sales Orders"), + fieldname: "sales_order", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Sales Order', txt); + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Item', txt); + } + }, + { + label: __("Age"), + fieldname:"age", + fieldtype: "Int", + default: "0" + }, + { + label: __("Charts Based On"), + fieldname:"charts_based_on", + fieldtype: "Select", + options: ["Status", "Age", "Quantity"], + default: "Status" + }, + ] +}; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.json b/erpnext/manufacturing/report/work_order_summary/work_order_summary.json new file mode 100644 index 00000000000..0d093e22e94 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "creation": "2020-04-17 17:07:56.830358", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-19 16:59:47.979278", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Work Order Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py new file mode 100644 index 00000000000..bc09ed43354 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -0,0 +1,270 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import date_diff, today, getdate, flt +from frappe import _ +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) + +def execute(filters=None): + columns, data = [], [] + + if not filters.get("age"): + filters["age"] = 0 + + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {"docstatus": 1} + + fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", + "planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] + + for field in ["sales_order", "production_item", "status", "company"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + query_filters["planned_start_date"] = (">=", filters.get("from_date")) + query_filters["planned_end_date"] = ("<=", filters.get("to_date")) + + data = frappe.get_all("Work Order", + fields= fields, filters=query_filters, order_by="planned_start_date asc") + + res = [] + for d in data: + start_date = d.actual_start_date or d.planned_start_date + d.age = 0 + + if d.status != 'Completed': + d.age = date_diff(today(), start_date) + + if filters.get("age") <= d.age: + res.append(d) + + return res + +def get_chart_data(data, filters): + if filters.get("charts_based_on") == "Status": + return get_chart_based_on_status(data) + elif filters.get("charts_based_on") == "Age": + return get_chart_based_on_age(data) + else: + return get_chart_based_on_qty(data, filters) + +def get_chart_based_on_status(data): + labels = ["Not Started", "In Process", "Stopped", "Completed"] + + status_wise_data = { + "Not Started": 0, + "In Process": 0, + "Stopped": 0, + "Completed": 0 + } + + for d in data: + if d.status == "In Process" and d.produced_qty: + status_wise_data["Completed"] += d.produced_qty + + status_wise_data[d.status] += d.qty + + values = [status_wise_data["Not Started"], status_wise_data["In Process"], + status_wise_data["Stopped"], status_wise_data["Completed"]] + + chart = { + "data": { + 'labels': labels, + 'datasets': [{'name':'Qty Wise Chart', 'values': values}] + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_chart_based_on_age(data): + labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] + + age_wise_data = { + "0-30 Days": 0, + "30-60 Days": 0, + "60-90 Days": 0, + "90 Above": 0 + } + + for d in data: + if d.age > 0 and d.age <= 30: + age_wise_data["0-30 Days"] += 1 + elif d.age > 30 and d.age <= 60: + age_wise_data["30-60 Days"] += 1 + elif d.age > 60 and d.age <= 90: + age_wise_data["60-90 Days"] += 1 + else: + age_wise_data["90 Above"] += 1 + + values = [age_wise_data["0-30 Days"], age_wise_data["30-60 Days"], + age_wise_data["60-90 Days"], age_wise_data["90 Above"]] + + chart = { + "data": { + 'labels': labels, + 'datasets': [{'name':'Qty Wise Chart', 'values': values}] + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_chart_based_on_qty(data, filters): + labels, periodic_data = prepare_chart_data(data, filters) + + pending, completed = [], [] + datasets = [] + + for d in labels: + pending.append(periodic_data.get("Pending").get(d)) + completed.append(periodic_data.get("Completed").get(d)) + + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "bar", + "barOptions": { + "stacked": 1 + } + } + + return chart + +def prepare_chart_data(data, filters): + labels = [] + + periodic_data = { + "Pending": {}, + "Completed": {} + } + + filters.range = "Monthly" + + ranges = get_period_date_ranges(filters) + for from_date, end_date in ranges: + period = get_period(end_date, filters) + if period not in labels: + labels.append(period) + + if period not in periodic_data["Pending"]: + periodic_data["Pending"][period] = 0 + + if period not in periodic_data["Completed"]: + periodic_data["Completed"][period] = 0 + + for d in data: + if getdate(d.planned_start_date) >= from_date and getdate(d.planned_start_date) <= end_date: + periodic_data["Pending"][period] += (flt(d.qty) - flt(d.produced_qty)) + periodic_data["Completed"][period] += flt(d.produced_qty) + + return labels, periodic_data + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + ] + + if not filters.get("status"): + columns.append( + { + "label": _("Status"), + "fieldname": "status", + "width": 100 + }, + ) + + columns.extend([ + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Produce Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Produced Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 90 + }, + { + "label": _("Planned Start Date"), + "fieldname": "planned_start_date", + "fieldtype": "Date", + "width": 150 + }, + { + "label": _("Planned End Date"), + "fieldname": "planned_end_date", + "fieldtype": "Date", + "width": 150 + } + ]) + + if filters.get("status") != 'Not Started': + columns.extend([ + { + "label": _("Actual Start Date"), + "fieldname": "actual_start_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Actual End Date"), + "fieldname": "actual_end_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Age"), + "fieldname": "age", + "fieldtype": "Float", + "width": 110 + }, + ]) + + if filters.get("status") == 'Completed': + columns.extend([ + { + "label": _("Lead Time (in mins)"), + "fieldname": "lead_time", + "fieldtype": "Float", + "width": 110 + }, + ]) + + return columns \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ebbcccc7109..dc88ffb6bea 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -684,3 +684,7 @@ execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation +erpnext.patches.v12_0.set_serial_no_status +erpnext.patches.v12_0.update_price_list_currency_in_bom +execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') +erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo diff --git a/erpnext/patches/v12_0/set_serial_no_status.py b/erpnext/patches/v12_0/set_serial_no_status.py new file mode 100644 index 00000000000..4ec84ef0f9e --- /dev/null +++ b/erpnext/patches/v12_0/set_serial_no_status.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, nowdate + +def execute(): + frappe.reload_doc('stock', 'doctype', 'serial_no') + + for serial_no in frappe.db.sql("""select name, delivery_document_type, warranty_expiry_date from `tabSerial No` + where (status is NULL OR status='')""", as_dict = 1): + if serial_no.get("delivery_document_type"): + status = "Delivered" + elif serial_no.get("warranty_expiry_date") and getdate(serial_no.get("warranty_expiry_date")) <= getdate(nowdate()): + status = "Expired" + else: + status = "Active" + + frappe.db.set_value("Serial No", serial_no.get("name"), "status", status) \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py new file mode 100644 index 00000000000..f5e7b947c23 --- /dev/null +++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, flt +from erpnext.setup.utils import get_exchange_rate + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "bom") + frappe.reload_doc("manufacturing", "doctype", "bom_item") + + frappe.db.sql(""" UPDATE `tabBOM`, `tabPrice List` + SET + `tabBOM`.price_list_currency = `tabPrice List`.currency, + `tabBOM`.plc_conversion_rate = 1.0 + WHERE + `tabBOM`.buying_price_list = `tabPrice List`.name AND `tabBOM`.docstatus < 2 + AND `tabBOM`.rm_cost_as_per = 'Price List' + """) + + for d in frappe.db.sql(""" + SELECT + bom.creation, bom.name, bom.price_list_currency as currency, + company.default_currency as company_currency + FROM + `tabBOM` as bom, `tabCompany` as company + WHERE + bom.company = company.name AND bom.rm_cost_as_per = 'Price List' AND + bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", as_dict=1): + plc_conversion_rate = get_exchange_rate(d.currency, + d.company_currency, getdate(d.creation), "for_buying") + + frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) \ No newline at end of file diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py index e69de29bb2d..baffc488252 100644 --- a/erpnext/patches/v13_0/__init__.py +++ b/erpnext/patches/v13_0/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py new file mode 100644 index 00000000000..331c5590e58 --- /dev/null +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -0,0 +1,42 @@ + +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.utils import add_to_date +from frappe.utils.dashboard import get_config, make_records + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + frappe.reload_doc("manufacturing", "doctype", "job_card") + + data = frappe.get_all("Work Order", + filters = { + "docstatus": 1, + "status": ("in", ["In Process", "Completed"]) + }) + + for d in data: + doc = frappe.get_doc("Work Order", d.name) + doc.set_actual_dates() + doc.db_set("actual_start_date", doc.actual_start_date, update_modified=False) + + if doc.status == "Completed": + frappe.db.set_value("Work Order", d.name, { + "actual_end_date": doc.actual_end_date, + "lead_time": doc.lead_time + }, update_modified=False) + + if not doc.planned_end_date: + planned_end_date = add_to_date(doc.planned_start_date, minutes=doc.lead_time) + doc.db_set("planned_end_date", doc.actual_start_date, update_modified=False) + + frappe.db.sql(""" UPDATE `tabJob Card` as jc, `tabWork Order` as wo + SET + jc.production_item = wo.production_item, jc.item_name = wo.item_name + WHERE + jc.work_order = wo.name and IFNULL(jc.production_item, "") = "" + """) \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json index 0849fd7aeb0..7691fe35870 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json @@ -1,10 +1,12 @@ { - "autoname": "format:MTNG-{date}", + "actions": [], + "autoname": "naming_series:", "creation": "2018-10-15 16:25:41.548432", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "naming_series", "date", "cb_00", "status", @@ -53,9 +55,16 @@ "fieldname": "sb_01", "fieldtype": "Section Break", "label": "Minutes" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "MTNG-.YYYY.-.MM.-.DD.-" } ], - "modified": "2019-07-13 19:57:40.500541", + "links": [], + "modified": "2020-05-19 13:18:59.821740", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Meeting", diff --git a/erpnext/selling/desk_page/selling/selling.json b/erpnext/selling/desk_page/selling/selling.json deleted file mode 100644 index a20806b264e..00000000000 --- a/erpnext/selling/desk_page/selling/selling.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "cards": [ - { - "hidden": 0, - "label": "Items and Pricing", - "links": "[\n {\n \"description\": \"All Products or Services.\",\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Price List\"\n ],\n \"description\": \"Multiple Item prices.\",\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"onboard\": 1,\n \"route\": \"#Report/Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Price List master.\",\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of Item Groups.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Bundle items at time of sale.\",\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying different promotional schemes.\",\n \"label\": \"Promotional Scheme\",\n \"name\": \"Promotional Scheme\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Rules for applying pricing and discount.\",\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for adding shipping costs.\",\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Define coupon codes.\",\n \"label\": \"Coupon Code\",\n \"name\": \"Coupon Code\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Settings", - "links": "[\n {\n \"description\": \"Default settings for selling transactions.\",\n \"label\": \"Selling Settings\",\n \"name\": \"Selling Settings\",\n \"settings\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Template of terms or contract.\",\n \"label\": \"Terms and Conditions Template\",\n \"name\": \"Terms and Conditions\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for selling transactions.\",\n \"label\": \"Sales Taxes and Charges Template\",\n \"name\": \"Sales Taxes and Charges Template\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Track Leads by Lead Source.\",\n \"label\": \"Lead Source\",\n \"name\": \"Lead Source\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Addresses.\",\n \"label\": \"Address\",\n \"name\": \"Address\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Other Reports", - "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Customer Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"route_options\": {\n \"party_type\": \"Customer\"\n },\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Search\",\n \"name\": \"BOM Search\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Available Stock for Packing Items\",\n \"name\": \"Available Stock for Packing Items\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Pending SO Items For Purchase Request\",\n \"name\": \"Pending SO Items For Purchase Request\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customers Without Any Sales Transactions\",\n \"name\": \"Customers Without Any Sales Transactions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n }\n]" - }, - { - "hidden": 0, - "label": "Sales", - "links": "[\n {\n \"description\": \"Customer Database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Quotes to Leads or Customers.\",\n \"label\": \"Quotation\",\n \"name\": \"Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Confirmed orders from Customers.\",\n \"label\": \"Sales Order\",\n \"name\": \"Sales Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Invoices for Costumers.\",\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Blanket Orders from Costumers.\",\n \"label\": \"Blanket Order\",\n \"name\": \"Blanket Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Manage Sales Partners.\",\n \"label\": \"Sales Partner\",\n \"name\": \"Sales Partner\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Ordered Items To Be Delivered\",\n \"name\": \"Ordered Items To Be Delivered\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n }\n]" - } - ], - "category": "Modules", - "charts": [ - { - "chart_name": "Income", - "label": "Income" - } - ], - "creation": "2020-01-28 11:49:12.092882", - "developer_mode_only": 0, - "disable_user_customization": 0, - "docstatus": 0, - "doctype": "Desk Page", - "extends_another_page": 0, - "icon": "", - "idx": 0, - "is_standard": 1, - "label": "Selling", - "modified": "2020-04-01 11:28:51.047373", - "modified_by": "Administrator", - "module": "Selling", - "name": "Selling", - "owner": "Administrator", - "pin_to_bottom": 0, - "pin_to_top": 0, - "shortcuts": [ - { - "label": "Sales Invoice", - "link_to": "Sales Invoice", - "type": "DocType" - }, - { - "label": "Sales Order", - "link_to": "Sales Order", - "type": "DocType" - }, - { - "label": "Quotation", - "link_to": "Quotation", - "type": "DocType" - }, - { - "label": "Delivery Note", - "link_to": "Delivery Note", - "type": "DocType" - }, - { - "label": "Accounts Receivable", - "link_to": "Accounts Receivable", - "type": "Report" - }, - { - "label": "Sales Register", - "link_to": "Sales Register", - "type": "Report" - } - ] -} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 3d172ac7a22..a6889e080d9 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -337,11 +337,15 @@ def get_loyalty_programs(doc): return lp_details def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): + from erpnext.controllers.queries import get_fields + if frappe.db.get_default("cust_master_name") == "Customer Name": fields = ["name", "customer_group", "territory"] else: fields = ["name", "customer_name", "customer_group", "territory"] + fields = get_fields("Customer", fields) + match_conditions = build_match_conditions("Customer") match_conditions = "and {}".format(match_conditions) if match_conditions else "" @@ -349,14 +353,17 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): filter_conditions = get_filters_cond(doctype, filters, []) match_conditions += "{}".format(filter_conditions) - return frappe.db.sql("""select %s from `tabCustomer` where docstatus < 2 - and (%s like %s or customer_name like %s) - {match_conditions} + return frappe.db.sql(""" + select %s + from `tabCustomer` + where docstatus < 2 + and (%s like %s or customer_name like %s) + {match_conditions} order by - case when name like %s then 0 else 1 end, - case when customer_name like %s then 0 else 1 end, - name, customer_name limit %s, %s""".format(match_conditions=match_conditions) % - (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), + case when name like %s then 0 else 1 end, + case when customer_name like %s then 0 else 1 end, + name, customer_name limit %s, %s + """.format(match_conditions=match_conditions) % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len)) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js index 85c0cd8bf0c..e3d0a55c3a0 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.js +++ b/erpnext/selling/page/sales_funnel/sales_funnel.js @@ -90,6 +90,10 @@ erpnext.SalesFunnel = class SalesFunnel { get_data(btn) { var me = this; + if (!this.company) { + frappe.throw(__("Please Select a Company.")); + } + const method_map = { "sales_funnel": "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data", "opp_by_lead_source": "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source", diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index d62e2093c69..dba24ef5b00 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -8,14 +8,23 @@ from frappe import _ from erpnext.accounts.report.utils import convert import pandas as pd +def validate_filters(from_date, to_date, company): + if from_date and to_date and (from_date >= to_date): + frappe.throw(_("To Date must be greater than From Date")) + + if not company: + frappe.throw(_("Please Select a Company")) + @frappe.whitelist() def get_funnel_data(from_date, to_date, company): + validate_filters(from_date, to_date, company) + active_leads = frappe.db.sql("""select count(*) from `tabLead` where (date(`modified`) between %s and %s) and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' + left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` @@ -38,6 +47,8 @@ def get_funnel_data(from_date, to_date, company): @frappe.whitelist() def get_opp_by_lead_source(from_date, to_date, company): + validate_filters(from_date, to_date, company) + opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source']) if opportunities: @@ -68,11 +79,13 @@ def get_opp_by_lead_source(from_date, to_date, company): @frappe.whitelist() def get_pipeline_data(from_date, to_date, company): + validate_filters(from_date, to_date, company) + opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability']) if opportunities: default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') - + cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index a9f3cd09ef5..c951066aa83 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-04-30 13:13:03", "doctype": "DocType", @@ -8,6 +9,7 @@ "field_order": [ "naming_series", "report_date", + "status", "column_break_4", "inspection_type", "reference_type", @@ -20,17 +22,16 @@ "column_break1", "item_name", "description", - "status", + "bom_no", + "specification_details", + "quality_inspection_template", + "readings", "section_break_14", "inspected_by", "verified_by", - "bom_no", "column_break_17", "remarks", - "amended_from", - "specification_details", - "quality_inspection_template", - "readings" + "amended_from" ], "fields": [ { @@ -231,7 +232,8 @@ "icon": "fa fa-search", "idx": 1, "is_submittable": 1, - "modified": "2019-07-12 12:07:23.153698", + "links": [], + "modified": "2020-04-26 17:50:25.068222", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 62c9eb1eb2c..18d68539daa 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -363,6 +363,9 @@ class StockEntry(StockController): + self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError) def set_incoming_rate(self): + if self.purpose == "Repack": + self.set_basic_rate_for_finished_goods() + for d in self.items: if d.s_warehouse: args = self.get_args_for_incoming_rate(d) @@ -475,20 +478,31 @@ class StockEntry(StockController): "allow_zero_valuation": item.allow_zero_valuation_rate, }) - def set_basic_rate_for_finished_goods(self, raw_material_cost, scrap_material_cost): + def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0): + total_fg_qty = 0 + if not raw_material_cost and self.get("items"): + raw_material_cost = sum([flt(row.basic_amount) for row in self.items + if row.s_warehouse and not row.t_warehouse]) + + total_fg_qty = sum([flt(row.qty) for row in self.items + if row.t_warehouse and not row.s_warehouse]) + if self.purpose in ["Manufacture", "Repack"]: for d in self.get("items"): if (d.transfer_qty and (d.bom_no or d.t_warehouse) and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)): - if self.work_order \ - and frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if (self.work_order and self.purpose == "Manufacture" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")): bom_items = self.get_bom_raw_materials(d.transfer_qty) raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) - if raw_material_cost: + if raw_material_cost and self.purpose == "Manufacture": d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate")) d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) + elif self.purpose == "Repack" and total_fg_qty: + d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) + d.basic_amount = d.basic_rate * d.qty def distribute_additional_costs(self): if self.purpose == "Material Issue": @@ -718,11 +732,15 @@ class StockEntry(StockController): pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc) pro_doc.run_method("update_status") + if self.fg_completed_qty: pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") + if not pro_doc.operations: + pro_doc.set_actual_dates() + def get_item_details(self, args=None, for_update=False): item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, i.has_batch_no, i.sample_quantity, i.has_serial_no, diff --git a/erpnext/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json index 0f15e4737fd..1df9fb79696 100644 --- a/erpnext/support/web_form/issues/issues.json +++ b/erpnext/support/web_form/issues/issues.json @@ -18,7 +18,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2020-03-06 05:24:05.749664", + "modified": "2020-05-19 13:01:10.729088", "modified_by": "Administrator", "module": "Support", "name": "issues", @@ -76,7 +76,7 @@ { "allow_read_on_all_link_options": 0, "fieldname": "description", - "fieldtype": "Text", + "fieldtype": "Text Editor", "hidden": 0, "label": "Description", "max_length": 0, diff --git a/erpnext/tests/test_woocommerce.py b/erpnext/tests/test_woocommerce.py index ce0f47d685f..df715ab2027 100644 --- a/erpnext/tests/test_woocommerce.py +++ b/erpnext/tests/test_woocommerce.py @@ -24,7 +24,7 @@ class TestWoocommerce(unittest.TestCase): woo_settings.creation_user = "Administrator" woo_settings.save(ignore_permissions=True) - def test_sales_order_for_woocommerece(self): + def test_sales_order_for_woocommerce(self): frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"Woocommerce","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]} order()