diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 00000000000..8f678583066 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,14 @@ +name: Trigger Docker build on release +on: + release: + types: [created] +jobs: + curl: + runs-on: ubuntu-latest + container: + image: alpine:latest + steps: + - name: curl + run: | + apk add curl bash + curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests diff --git a/CODEOWNERS b/CODEOWNERS index 5e1113d34f9..7cf65a7a732 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,17 +3,16 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -* @nabinhait -manufacturing/ @rohitwaghchaure +manufacturing/ @rohitwaghchaure @marination accounts/ @deepeshgarg007 @nextchamp-saqib -loan_management/ @deepeshgarg007 -pos* @nextchamp-saqib -assets/ @nextchamp-saqib +loan_management/ @deepeshgarg007 @rohitwaghchaure +pos* @nextchamp-saqib @rohitwaghchaure +assets/ @nextchamp-saqib @deepeshgarg007 stock/ @marination @rohitwaghchaure -buying/ @marination @rohitwaghchaure -hr/ @Anurag810 -projects/ @hrwX -support/ @hrwX -healthcare/ @ruchamahabal -erpnext_integrations/ @Mangesh-Khairnar +buying/ @marination @deepeshgarg007 +hr/ @Anurag810 @rohitwaghchaure +projects/ @hrwX @nextchamp-saqib +support/ @hrwX @marination +healthcare/ @ruchamahabal @marination +erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib requirements.txt @gavindsouza diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index 421c86dba01..b2abffc79d7 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -5,7 +5,22 @@ 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 +from erpnext.accounts.utils import get_fiscal_year, get_account_name, FiscalYearError + +def _get_fiscal_year(date=None): + try: + fiscal_year = get_fiscal_year(date=nowdate(), as_dict=True) + return fiscal_year + + except FiscalYearError: + #if no fiscal year for current date then get default fiscal year + try: + fiscal_year = get_fiscal_year(as_dict=True) + return fiscal_year + + except FiscalYearError: + #if still no fiscal year found then no accounting data created, return + return None def get_company_for_dashboards(): company = frappe.defaults.get_defaults().company @@ -18,10 +33,16 @@ def get_company_for_dashboards(): return None def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards() + "charts": get_charts(fiscal_year), + "number_cards": get_number_cards(fiscal_year) }) def get_dashboards(): @@ -46,10 +67,9 @@ def get_dashboards(): ] }] -def get_charts(): +def get_charts(fiscal_year): 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 [ @@ -61,8 +81,8 @@ def get_charts(): "filters_json": json.dumps({ "company": company.name, "filter_based_on": "Fiscal Year", - "from_fiscal_year": fiscal_year[0], - "to_fiscal_year": fiscal_year[0], + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), "periodicity": "Monthly", "include_default_book_entries": 1 }), @@ -158,8 +178,8 @@ def get_charts(): "report_name": "Budget Variance Report", "filters_json": json.dumps({ "company": company.name, - "from_fiscal_year": fiscal_year[0], - "to_fiscal_year": fiscal_year[0], + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), "period": "Monthly", "budget_against": "Cost Center" }), @@ -190,10 +210,10 @@ def get_charts(): }, ] -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]) +def get_number_cards(fiscal_year): + + year_start_date = get_date_str(fiscal_year.get("year_start_date")) + year_end_date = get_date_str(fiscal_year.get("year_end_date")) return [ { "doctype": "Number Card", diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index f62d07668de..28b090bdadb 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -14,6 +14,9 @@ frappe.treeview_settings["Account"] = { on_change: function() { var me = frappe.treeview_settings['Account'].treeview; var company = me.page.fields_dict.company.get_value(); + if (!company) { + frappe.throw(__("Please set a Company")); + } frappe.call({ method: "erpnext.accounts.doctype.account.account.get_root_company", args: { diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 894ec5bdec5..8834385135f 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -72,7 +72,11 @@ def make_dimension_in_accounting_doctypes(doc): if doctype == "Budget": add_dimension_to_budget_doctype(df, doc) else: - create_custom_field(doctype, df) + meta = frappe.get_meta(doctype, cached=False) + fieldnames = [d.fieldname for d in meta.get("fields")] + + if df['fieldname'] not in fieldnames: + create_custom_field(doctype, df) count += 1 diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 9e2f6eed3b6..f341f782078 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -14,7 +14,18 @@ frappe.ui.form.on('Cost Center', { is_group: 1 } } - }) + }); + + frm.set_query("cost_center", "distributed_cost_center", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + enable_distributed_cost_center: 0, + name: ['!=', frm.doc.name] + } + }; + }); }, refresh: function(frm) { if (!frm.is_new()) { diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 5013c92a327..e7fa954e018 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -16,6 +16,9 @@ "cb0", "is_group", "disabled", + "section_break_9", + "enable_distributed_cost_center", + "distributed_cost_center", "lft", "rgt", "old_parent" @@ -119,13 +122,31 @@ "fieldname": "disabled", "fieldtype": "Check", "label": "Disabled" + }, + { + "default": "0", + "fieldname": "enable_distributed_cost_center", + "fieldtype": "Check", + "label": "Enable Distributed Cost Center" + }, + { + "depends_on": "eval:doc.is_group==0", + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "depends_on": "enable_distributed_cost_center", + "fieldname": "distributed_cost_center", + "fieldtype": "Table", + "label": "Distributed Cost Center", + "options": "Distributed Cost Center" } ], "icon": "fa fa-money", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-04-29 16:09:30.025214", + "modified": "2020-06-17 16:09:30.025214", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center", diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 0294e78111c..12094d4f989 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -19,6 +19,24 @@ class CostCenter(NestedSet): def validate(self): self.validate_mandatory() self.validate_parent_cost_center() + self.validate_distributed_cost_center() + + def validate_distributed_cost_center(self): + if cint(self.enable_distributed_cost_center): + if not self.distributed_cost_center: + frappe.throw(_("Please enter distributed cost center")) + if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100: + frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100")) + if not self.get('__islocal'): + if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \ + and self.check_if_part_of_distributed_cost_center(): + frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center")) + if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False): + frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center")) + if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)): + frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table.")) + else: + self.distributed_cost_center = [] def validate_mandatory(self): if self.cost_center_name != self.company and not self.parent_cost_center: @@ -43,12 +61,15 @@ class CostCenter(NestedSet): return 1 def convert_ledger_to_group(self): + if cint(self.enable_distributed_cost_center): + frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) + if self.check_if_part_of_distributed_cost_center(): + frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group")) if self.check_gle_exists(): frappe.throw(_("Cost Center with existing transactions can not be converted to group")) - else: - self.is_group = 1 - self.save() - return 1 + self.is_group = 1 + self.save() + return 1 def check_gle_exists(self): return frappe.db.get_value("GL Entry", {"cost_center": self.name}) @@ -57,6 +78,9 @@ class CostCenter(NestedSet): return frappe.db.sql("select name from `tabCost Center` where \ parent_cost_center = %s and docstatus != 2", self.name) + def check_if_part_of_distributed_cost_center(self): + return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name}) + def before_rename(self, olddn, newdn, merge=False): # Add company abbr if not provided from erpnext.setup.doctype.company.company import get_name_with_abbr @@ -100,3 +124,7 @@ def get_name_with_number(new_account, account_number): if account_number and not new_account[0].isdigit(): new_account = account_number + " - " + new_account return new_account + +def check_if_distributed_cost_center_enabled(cost_center_list): + value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1) + return next((True for x in value_list if x[0]), False) \ No newline at end of file diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py index 8f23d906760..b5fc7e3b497 100644 --- a/erpnext/accounts/doctype/cost_center/test_cost_center.py +++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py @@ -22,6 +22,33 @@ class TestCostCenter(unittest.TestCase): self.assertRaises(frappe.ValidationError, cost_center.save) + def test_validate_distributed_cost_center(self): + + if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}): + frappe.get_doc(test_records[0]).insert() + + if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}): + frappe.get_doc(test_records[1]).insert() + + invalid_distributed_cost_center = frappe.get_doc({ + "company": "_Test Company", + "cost_center_name": "_Test Distributed Cost Center", + "doctype": "Cost Center", + "is_group": 0, + "parent_cost_center": "_Test Company - _TC", + "enable_distributed_cost_center": 1, + "distributed_cost_center": [{ + "cost_center": "_Test Cost Center - _TC", + "percentage_allocation": 40 + }, { + "cost_center": "_Test Cost Center 2 - _TC", + "percentage_allocation": 50 + } + ] + }) + + self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save) + def create_cost_center(**args): args = frappe._dict(args) if args.cost_center_name: diff --git a/erpnext/support/doctype/service_level/__init__.py b/erpnext/accounts/doctype/distributed_cost_center/__init__.py similarity index 100% rename from erpnext/support/doctype/service_level/__init__.py rename to erpnext/accounts/doctype/distributed_cost_center/__init__.py diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json new file mode 100644 index 00000000000..45b0e2df9ba --- /dev/null +++ b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2020-03-19 12:34:01.500390", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cost_center", + "percentage_allocation" + ], + "fields": [ + { + "fieldname": "cost_center", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Cost Center", + "options": "Cost Center", + "reqd": 1 + }, + { + "fieldname": "percentage_allocation", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Percentage Allocation", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-03-19 12:54:43.674655", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Distributed Cost Center", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py new file mode 100644 index 00000000000..48c589f0c0f --- /dev/null +++ b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class DistributedCostCenter(Document): + pass diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 9d5063929fd..af2aa65e6b2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -191,6 +191,7 @@ { "fieldname": "total_debit", "fieldtype": "Currency", + "in_list_view": 1, "label": "Total Debit", "no_copy": 1, "oldfieldname": "total_debit", @@ -252,7 +253,6 @@ "fieldname": "total_amount", "fieldtype": "Currency", "hidden": 1, - "in_list_view": 1, "label": "Total Amount", "no_copy": 1, "options": "total_amount_currency", @@ -503,7 +503,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2020-04-29 10:55:28.240916", + "modified": "2020-06-02 18:15:46.955697", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 26c84a6398c..ff3533a6792 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -18,6 +18,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "currency_section", "account_currency", "column_break_10", @@ -32,7 +33,6 @@ "reference_type", "reference_name", "reference_due_date", - "project", "col_break3", "is_advance", "user_remark", @@ -273,7 +273,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-25 01:47:49.060128", + "modified": "2020-06-18 14:06:54.833738", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cfcefffc758..bd15f76f12c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -319,7 +319,7 @@ class PaymentEntry(AccountsController): invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map[key] += reference.allocated_amount - if not invoice_paid_amount_map.get(reference.reference_name): + if not invoice_paid_amount_map.get(key): payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, fields=['paid_amount', 'payment_amount', 'payment_term']) for term in payment_schedule: @@ -332,12 +332,14 @@ class PaymentEntry(AccountsController): frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) else: - outstanding = invoice_paid_amount_map.get(key)['outstanding'] + outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding')) + if amount > outstanding: frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) - frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s - WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + if amount and outstanding: + frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s + WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) def set_status(self): if self.docstatus == 2: @@ -1091,17 +1093,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): references = [] for payment_term in payment_schedule: - references.append({ - 'reference_doctype': dt, - 'reference_name': dn, - 'bill_no': doc.get('bill_no'), - 'due_date': doc.get('due_date'), - 'total_amount': grand_total, - 'outstanding_amount': outstanding_amount, - 'payment_term': payment_term.payment_term, - 'allocated_amount': flt(payment_term.payment_amount - payment_term.paid_amount, + payment_term_outstanding = flt(payment_term.payment_amount - payment_term.paid_amount, payment_term.precision('payment_amount')) - }) + + if payment_term_outstanding: + references.append({ + 'reference_doctype': dt, + 'reference_name': dn, + 'bill_no': doc.get('bill_no'), + 'due_date': doc.get('due_date'), + 'total_amount': grand_total, + 'outstanding_amount': outstanding_amount, + 'payment_term': payment_term.payment_term, + 'allocated_amount': payment_term_outstanding + }) return references diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 7508683c080..eef6be1a7a1 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -349,9 +349,10 @@ "read_only": 1 } ], + "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-08 10:23:02.815237", + "modified": "2020-05-29 17:38:49.392713", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index a80637e7a45..eb1ccd95afb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -43,6 +43,8 @@ "col_break_address", "shipping_address", "shipping_address_display", + "billing_address", + "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -169,7 +171,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -181,7 +185,9 @@ "options": "ACC-PINV-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier", @@ -193,7 +199,9 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -205,7 +213,9 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "supplier.tax_id", @@ -213,21 +223,27 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -235,19 +251,25 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -257,13 +279,17 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1 + "remember_last_selected_value": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", @@ -275,7 +301,9 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "posting_time", @@ -284,6 +312,8 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -292,7 +322,9 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -304,44 +336,58 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date" + "label": "Release Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold" + "label": "Reason For Putting On Hold", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details" + "label": "Supplier Invoice Details", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_no", @@ -349,11 +395,15 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_date", @@ -361,13 +411,17 @@ "label": "Supplier Invoice Date", "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns" + "label": "Returns", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", @@ -377,26 +431,34 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -404,51 +466,67 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -457,7 +535,9 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "conversion_rate", @@ -466,18 +546,24 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -485,14 +571,18 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -501,11 +591,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -513,7 +607,9 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -523,11 +619,15 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -535,7 +635,9 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_subcontracted==\"Yes\"", @@ -546,25 +648,33 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -574,42 +684,56 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied" + "label": "Raw Materials Supplied", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", "options": "Purchase Receipt Item Supplied", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_26", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -617,7 +741,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -627,18 +753,24 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -648,42 +780,56 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_49", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -692,7 +838,9 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -700,13 +848,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -715,13 +867,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -731,7 +887,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -741,7 +899,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -751,11 +911,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_40", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_added", @@ -765,7 +929,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -775,7 +941,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -783,14 +951,18 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -798,7 +970,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -806,28 +980,38 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_49", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -837,7 +1021,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounding_adjustment", @@ -846,7 +1032,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -856,7 +1044,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_in_words", @@ -865,13 +1055,17 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -882,7 +1076,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounding_adjustment", @@ -891,7 +1087,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -901,7 +1099,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -910,7 +1110,9 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_advance", @@ -921,7 +1123,9 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "outstanding_amount", @@ -932,14 +1136,18 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -947,30 +1155,40 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments" + "label": "Payments", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "clearance_date", "fieldtype": "Date", "hidden": 1, - "label": "Clearance Date" + "label": "Clearance Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_paid", @@ -979,7 +1197,9 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_paid_amount", @@ -988,7 +1208,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -996,7 +1218,9 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off" + "label": "Write Off", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "write_off_amount", @@ -1004,7 +1228,9 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_write_off_amount", @@ -1013,11 +1239,15 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1025,7 +1255,9 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1033,7 +1265,9 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1043,13 +1277,17 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)" + "label": "Set Advances and Allocate (FIFO)", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1057,7 +1295,9 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advances", @@ -1067,20 +1307,26 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template" + "options": "Payment Terms Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -1088,7 +1334,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1096,25 +1344,33 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1" + "label": "Terms and Conditions1", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings" + "label": "Printing Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1122,7 +1378,9 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1130,11 +1388,15 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_112", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1146,14 +1408,18 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1162,7 +1428,9 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "credit_to", @@ -1173,7 +1441,9 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -1183,7 +1453,9 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -1193,7 +1465,9 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "against_expense_account", @@ -1203,11 +1477,15 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -1216,14 +1494,18 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_invoice_reference", "fieldtype": "Link", "label": "Inter Company Invoice Reference", "options": "Sales Invoice", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "remarks", @@ -1232,14 +1514,18 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1248,7 +1534,9 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1257,11 +1545,15 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -1270,24 +1562,32 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions " + "label": "Accounting Dimensions ", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1295,7 +1595,9 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "project", @@ -1309,14 +1611,32 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-05-29 13:05:25.199832", + "modified": "2020-06-13 22:26:30.800199", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f60f87f40ff..b2b4cb1ea76 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -582,14 +582,14 @@ class SalesInvoice(SellingController): def validate_item_code(self): for d in self.get('items'): - if not d.item_code: + if not d.item_code and self.is_opening == "No": msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) def validate_warehouse(self): super(SalesInvoice, self).validate_warehouse() for d in self.get_item_list(): - if not d.warehouse and frappe.get_cached_value("Item", d.item_code, "is_stock_item"): + if not d.warehouse and d.item_code and frappe.get_cached_value("Item", d.item_code, "is_stock_item"): frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code)) def validate_delivery_note(self): @@ -1454,11 +1454,17 @@ def get_inter_company_details(doc, doctype): parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}) company = frappe.get_cached_value("Customer", doc.customer, "represents_company") + if not parties: + frappe.throw(_('No Supplier found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company))) + party = get_internal_party(parties, "Supplier", doc) else: parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}) company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company") + if not parties: + frappe.throw(_('No Customer found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company))) + party = get_internal_party(parties, "Customer", doc) return { diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index a2f9040670f..8dbf0a84fa5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1772,53 +1772,6 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_deferred_error_email(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") - - item = create_item("_Test Item for Deferred Accounting") - item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account - item.no_of_months = 12 - item.save() - - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True) - si.items[0].enable_deferred_revenue = 1 - si.items[0].service_start_date = "2019-01-10" - si.items[0].service_end_date = "2019-03-15" - si.items[0].deferred_revenue_account = deferred_account - si.save() - si.submit() - - from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income - - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.acc_frozen_upto = '2019-01-31' - acc_settings.save() - - pda = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) - - pda.insert() - pda.submit() - - email = frappe.db.sql(""" select name from `tabEmail Queue` - where message like %(txt)s """, { - 'txt': "%%%s%%" % "Error while processing deferred accounting for {0}".format(pda.name) - }) - - self.assertTrue(email) - - acc_settings.load_from_db() - acc_settings.acc_frozen_upto = None - acc_settings.save() - def test_inter_company_transaction(self): if not frappe.db.exists("Customer", "_Test Internal Customer"): diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index b2638c78279..d32a348741e 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -45,7 +45,9 @@ class ShippingRule(Document): shipping_amount = 0.0 by_value = False - self.validate_countries(doc) + if doc.get_shipping_address(): + # validate country only if there is address + self.validate_countries(doc) if self.calculate_based_on == 'Net Total': value = doc.base_net_total diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index d40e58b5287..66aa18058b6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -169,9 +169,11 @@ class ReceivablePayableReport(object): def append_subtotal_row(self, party): sub_total_row = self.total_row_map.get(party) - self.data.append(sub_total_row) - self.data.append({}) - self.update_sub_total_row(sub_total_row, 'Total') + + if sub_total_row: + self.data.append(sub_total_row) + self.data.append({}) + self.update_sub_total_row(sub_total_row, 'Total') def get_voucher_balance(self, gle): if self.filters.get("sales_person"): @@ -232,7 +234,8 @@ class ReceivablePayableReport(object): if self.filters.get('group_by_party'): self.append_subtotal_row(self.previous_party) - self.data.append(self.total_row_map.get('Total')) + if self.data: + self.data.append(self.total_row_map.get('Total')) def append_row(self, row): self.allocate_future_payments(row) 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 05dc2826611..9c9ada871c8 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -29,37 +29,60 @@ def execute(filters=None): for dimension in dimensions: dimension_items = cam_map.get(dimension) if dimension_items: - for account, monthwise_data in iteritems(dimension_items): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate(["target", "actual", "variance"]): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value - - period_data[0] += last_total - - if filters.get("show_cumulative"): - last_total = period_data[0] - period_data[1] - - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly": - row += totals - data.append(row) + data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0) + else: + DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation + FROM `tabDistributed Cost Center` + WHERE cost_center IN %(dimension)s + AND parent NOT IN %(dimension)s + GROUP BY parent''',{'dimension':[dimension]}) + if DCC_allocation: + filters['budget_against_filter'] = [DCC_allocation[0][0]] + cam_map = get_dimension_account_month_map(filters) + dimension_items = cam_map.get(DCC_allocation[0][0]) + if dimension_items: + data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1]) chart = get_chart_data(filters, columns, data) return columns, data, None, chart +def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): + + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] + totals = [0, 0, 0] + for year in get_fiscal_years(filters): + last_total = 0 + for relevant_months in period_month_ranges: + period_data = [0, 0, 0] + for month in relevant_months: + if monthwise_data.get(year[0]): + month_data = monthwise_data.get(year[0]).get(month, {}) + for i, fieldname in enumerate(["target", "actual", "variance"]): + value = flt(month_data.get(fieldname)) + period_data[i] += value + totals[i] += value + + period_data[0] += last_total + + if DCC_allocation: + period_data[0] = period_data[0]*(DCC_allocation/100) + period_data[1] = period_data[1]*(DCC_allocation/100) + + if(filters.get("show_cumulative")): + last_total = period_data[0] - period_data[1] + + period_data[2] = period_data[0] - period_data[1] + row += period_data + totals[2] = totals[0] - totals[1] + if filters["period"] != "Yearly" : + row += totals + data.append(row) + + return data + + def get_columns(filters): columns = [ { @@ -366,7 +389,7 @@ def get_chart_data(filters, columns, data): budget_values[i] += values[index] actual_values[i] += values[index+1] index += 3 - + return { 'data': { 'labels': labels, diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 4a35a668658..533685d703d 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -56,9 +56,8 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ to_date = add_months(start_date, months_to_add) start_date = to_date - if to_date == get_first_day(to_date): - # if to_date is the first day, get the last day of previous month - to_date = add_days(to_date, -1) + # Subtract one day from to_date, as it may be first day in next fiscal year or month + to_date = add_days(to_date, -1) if to_date <= year_end_date: # the normal case @@ -387,11 +386,43 @@ def set_gl_entries_by_account( key: value }) + distributed_cost_center_query = "" + if filters and filters.get('cost_center'): + distributed_cost_center_query = """ + UNION ALL + SELECT posting_date, + account, + debit*(DCC_allocation.percentage_allocation/100) as debit, + credit*(DCC_allocation.percentage_allocation/100) as credit, + is_opening, + fiscal_year, + debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, + credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency, + account_currency + FROM `tabGL Entry`, + ( + SELECT parent, sum(percentage_allocation) as percentage_allocation + FROM `tabDistributed Cost Center` + WHERE cost_center IN %(cost_center)s + AND parent NOT IN %(cost_center)s + AND is_cancelled = 0 + GROUP BY parent + ) as DCC_allocation + WHERE company=%(company)s + {additional_conditions} + AND posting_date <= %(to_date)s + AND cost_center = DCC_allocation.parent + """.format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", '')) + gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {additional_conditions} and posting_date <= %(to_date)s - order by account, posting_date""".format(additional_conditions=additional_conditions), gl_filters, as_dict=True) #nosec + and is_cancelled = 0 + {distributed_cost_center_query} + order by account, posting_date""".format( + additional_conditions=additional_conditions, + distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec if filters and filters.get('presentation_currency'): convert_to_presentation_currency(gl_entries, get_currency(filters)) @@ -489,4 +520,4 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None): "width": 150 }) - return columns + return columns \ No newline at end of file diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index f83a2595f6a..fcd36e4e6e2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -128,18 +128,53 @@ def get_gl_entries(filters): filters['company_fb'] = frappe.db.get_value("Company", filters.get("company"), 'default_finance_book') + distributed_cost_center_query = "" + if filters and filters.get('cost_center'): + select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, + credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ + + distributed_cost_center_query = """ + UNION ALL + SELECT name as gl_entry, + posting_date, + account, + party_type, + party, + voucher_type, + voucher_no, + cost_center, project, + against_voucher_type, + against_voucher, + account_currency, + remarks, against, + is_opening, `tabGL Entry`.creation {select_fields_with_percentage} + FROM `tabGL Entry`, + ( + SELECT parent, sum(percentage_allocation) as percentage_allocation + FROM `tabDistributed Cost Center` + WHERE cost_center IN %(cost_center)s + AND parent NOT IN %(cost_center)s + GROUP BY parent + ) as DCC_allocation + WHERE company=%(company)s + {conditions} + AND posting_date <= %(to_date)s + AND cost_center = DCC_allocation.parent + """.format(select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) + gl_entries = frappe.db.sql( """ select name as gl_entry, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, project, against_voucher_type, against_voucher, account_currency, - remarks, against, is_opening {select_fields} + remarks, against, is_opening, creation {select_fields} from `tabGL Entry` where company=%(company)s {conditions} + {distributed_cost_center_query} {order_by_statement} """.format( - select_fields=select_fields, conditions=get_conditions(filters), + select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), filters, as_dict=1) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 9777ed1dfde..3445df7206f 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -265,13 +265,6 @@ def get_columns(additional_table_columns, filters): 'fieldtype': 'Currency', 'options': 'currency', 'width': 100 - }, - { - 'fieldname': 'currency', - 'label': _('Currency'), - 'fieldtype': 'Currency', - 'width': 80, - 'hidden': 1 } ] diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index bb78ee2d675..a05dcd75ce5 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -223,7 +223,7 @@ def get_columns(additional_table_columns, filters): } ] - if filters.get('group_by') != 'Terriotory': + if filters.get('group_by') != 'Territory': columns.extend([ { 'label': _("Territory"), @@ -304,13 +304,6 @@ def get_columns(additional_table_columns, filters): 'fieldtype': 'Currency', 'options': 'currency', 'width': 100 - }, - { - 'fieldname': 'currency', - 'label': _('Currency'), - 'fieldtype': 'Currency', - 'width': 80, - 'hidden': 1 } ] @@ -536,6 +529,13 @@ def get_tax_accounts(item_list, columns, company_currency, 'fieldtype': 'Currency', 'options': 'currency', 'width': 100 + }, + { + 'fieldname': 'currency', + 'label': _('Currency'), + 'fieldtype': 'Currency', + 'width': 80, + 'hidden': 1 } ] diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 6e9b31f2f6d..60e675f2f1f 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -105,6 +105,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name): def prepare_data(accounts, filters, total_row, parent_children_map, based_on): data = [] + new_accounts = accounts company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") for d in accounts: @@ -118,6 +119,19 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on): "currency": company_currency, "based_on": based_on } + if based_on == 'cost_center': + cost_center_doc = frappe.get_doc("Cost Center",d.name) + if not cost_center_doc.enable_distributed_cost_center: + DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation + FROM `tabDistributed Cost Center` + WHERE cost_center IN %(cost_center)s + AND parent NOT IN %(cost_center)s + GROUP BY parent""",{'cost_center': [d.name]}) + if DCC_allocation: + for account in new_accounts: + if account['name'] == DCC_allocation[0][0]: + for value in value_fields: + d[value] += account[value]*(DCC_allocation[0][1]/100) for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 4ac0f656119..a9fb237a048 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -111,7 +111,7 @@ def get_gle_map(filters): # {"purchase_invoice": list of dict of all gle created for this invoice} gle_map = {} gle = frappe.db.get_all('GL Entry',\ - {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]]}, + {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]], 'is_cancelled': 0}, ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"]) for d in gle: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 58cd5708f66..830bed27600 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -57,6 +57,9 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb frappe.cache().hset("fiscal_years", company, fiscal_years) + if not transaction_date and not fiscal_year: + return fiscal_years + if transaction_date: transaction_date = getdate(transaction_date) @@ -79,6 +82,23 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb if verbose==1: frappe.msgprint(error_msg) raise FiscalYearError(error_msg) +@frappe.whitelist() +def get_fiscal_year_filter_field(company=None): + field = { + "fieldtype": "Select", + "options": [], + "operator": "Between", + "query_value": True + } + fiscal_years = get_fiscal_years(company=company) + for fiscal_year in fiscal_years: + field["options"].append({ + "label": fiscal_year.name, + "value": fiscal_year.name, + "query_value": [fiscal_year.year_start_date.strftime("%Y-%m-%d"), fiscal_year.year_end_date.strftime("%Y-%m-%d")] + }) + return field + def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None): years = [f[0] for f in get_fiscal_years(date, label=_(label), company=company)] if fiscal_year not in years: @@ -935,4 +955,4 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) - return gl_entries \ No newline at end of file + return gl_entries diff --git a/erpnext/assets/dashboard_fixtures.py b/erpnext/assets/dashboard_fixtures.py index 9af45d16b62..7f3c1de406a 100644 --- a/erpnext/assets/dashboard_fixtures.py +++ b/erpnext/assets/dashboard_fixtures.py @@ -5,14 +5,23 @@ 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 - +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year +from erpnext.buying.dashboard_fixtures import get_company_for_dashboards def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + year_start_date = get_date_str(fiscal_year.get('year_start_date')) + year_end_date = get_date_str(fiscal_year.get('year_end_date')) + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), + "charts": get_charts(fiscal_year, year_start_date, year_end_date), + "number_cards": get_number_cards(fiscal_year, year_start_date, year_end_date), }) def get_dashboards(): @@ -31,12 +40,7 @@ def get_dashboards(): ] }] -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]) - - -def get_charts(): +def get_charts(fiscal_year, year_start_date, year_end_date): company = get_company_for_dashboards() return [ { @@ -55,8 +59,8 @@ def get_charts(): "company": company, "status": "In Location", "filter_based_on": "Fiscal Year", - "from_fiscal_year": fiscal_year[0], - "to_fiscal_year": fiscal_year[0], + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), "period_start_date": year_start_date, "period_end_date": year_end_date, "date_based_on": "Purchase Date", @@ -134,7 +138,7 @@ def get_charts(): } ] -def get_number_cards(): +def get_number_cards(fiscal_year, year_start_date, year_end_date): return [ { "name": "Total Assets", @@ -172,14 +176,4 @@ def get_number_cards(): "filters_json": "[]", "doctype": "Number Card" } - ] - -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 \ No newline at end of file + ] \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index d6adde6a371..1869a29c8dd 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -41,7 +41,7 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex team_member = frappe.db.get_value('User', assign_to_member, "email") args = { 'doctype' : 'Asset Maintenance', - 'assign_to' : team_member, + 'assign_to' : [team_member], 'name' : asset_maintenance_name, 'description' : maintenance_task, 'date' : next_due_date diff --git a/erpnext/buying/dashboard_fixtures.py b/erpnext/buying/dashboard_fixtures.py index 186bfb23af9..172c936bd2b 100644 --- a/erpnext/buying/dashboard_fixtures.py +++ b/erpnext/buying/dashboard_fixtures.py @@ -5,13 +5,24 @@ import frappe import json from frappe import _ from frappe.utils import nowdate -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + company = frappe.get_doc("Company", get_company_for_dashboards()) + fiscal_year_name = fiscal_year.get("name") + start_date = str(fiscal_year.get("year_start_date")) + end_date = str(fiscal_year.get("year_end_date")) + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), + "charts": get_charts(company, fiscal_year_name, start_date, end_date), + "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date), }) def get_company_for_dashboards(): @@ -24,12 +35,6 @@ def get_company_for_dashboards(): return company_list[0].name return None -company = frappe.get_doc("Company", get_company_for_dashboards()) -fiscal_year = get_fiscal_year(nowdate(), as_dict=1) -fiscal_year_name = fiscal_year.get("name") -start_date = str(fiscal_year.get("year_start_date")) -end_date = str(fiscal_year.get("year_end_date")) - def get_dashboards(): return [{ "name": "Buying", @@ -48,7 +53,7 @@ def get_dashboards(): ] }] -def get_charts(): +def get_charts(company, fiscal_year_name, start_date, end_date): return [ { "name": "Purchase Order Analysis", @@ -139,7 +144,7 @@ def get_charts(): } ] -def get_number_cards(): +def get_number_cards(company, fiscal_year_name, start_date, end_date): return [ { "name": "Annual Purchase", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index a4f60fbba5c..13f5cb0c818 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -38,6 +38,8 @@ "col_break_address", "shipping_address", "shipping_address_display", + "billing_address", + "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -135,7 +137,9 @@ { "fieldname": "supplier_section", "fieldtype": "Section Break", - "options": "fa fa-user" + "options": "fa fa-user", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -145,7 +149,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -157,7 +163,9 @@ "options": "PUR-ORD-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -170,14 +178,18 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))", "description": "Fetch items based on Default Supplier.", "fieldname": "get_items_from_open_material_requests", "fieldtype": "Button", - "label": "Get Items from Open Material Requests" + "label": "Get Items from Open Material Requests", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -186,7 +198,9 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Supplier Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -198,13 +212,17 @@ "options": "Company", "print_hide": 1, "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -216,27 +234,35 @@ "oldfieldname": "transaction_date", "oldfieldtype": "Date", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "schedule_date", "fieldtype": "Date", - "label": "Required By" + "label": "Required By", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval:doc.docstatus===1", "fieldname": "order_confirmation_no", "fieldtype": "Data", - "label": "Order Confirmation No" + "label": "Order Confirmation No", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval:doc.order_confirmation_no", "fieldname": "order_confirmation_date", "fieldtype": "Date", - "label": "Order Confirmation Date" + "label": "Order Confirmation Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -248,19 +274,25 @@ "oldfieldtype": "Data", "options": "Purchase Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "drop_ship", "fieldtype": "Section Break", - "label": "Drop Ship" + "label": "Drop Ship", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer", "fieldtype": "Link", "label": "Customer", "options": "Customer", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -268,31 +300,41 @@ "fieldtype": "Data", "label": "Customer Name", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_19", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_person", "fieldtype": "Link", "label": "Customer Contact", - "options": "Contact" + "options": "Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_display", "fieldtype": "Small Text", "hidden": 1, "label": "Customer Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_mobile", "fieldtype": "Small Text", "hidden": 1, "label": "Customer Mobile No", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_email", @@ -300,46 +342,60 @@ "hidden": 1, "label": "Customer Contact Email", "options": "Email", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "in_global_search": 1, "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", @@ -347,32 +403,42 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -382,7 +448,9 @@ "oldfieldtype": "Select", "options": "Currency", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "conversion_rate", @@ -392,18 +460,24 @@ "oldfieldtype": "Currency", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_price_list", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -411,14 +485,18 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -427,11 +505,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Sets 'Warehouse' in each row of the Items table.", @@ -439,11 +521,15 @@ "fieldtype": "Link", "label": "Set Target Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -452,25 +538,33 @@ "in_standard_filter": 1, "label": "Supply Raw Materials", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_subcontracted==\"Yes\"", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -480,26 +574,34 @@ "oldfieldname": "po_details", "oldfieldtype": "Table", "options": "Purchase Order Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_break_48", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Purchase Order Pricing Rule", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_material_details", "fieldtype": "Section Break", - "label": "Raw Materials Supplied" + "label": "Raw Materials Supplied", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplied_items", @@ -509,17 +611,23 @@ "oldfieldtype": "Table", "options": "Purchase Order Item Supplied", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sb_last_purchase", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -527,7 +635,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -538,18 +648,24 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_26", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -559,20 +675,26 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -581,22 +703,30 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_50", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_52", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -604,13 +734,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -619,13 +753,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -635,7 +773,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -645,7 +785,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -656,11 +798,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_39", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_added", @@ -670,7 +816,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -680,7 +828,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -688,14 +838,18 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "discount_section", "fieldtype": "Section Break", - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -703,7 +857,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -711,28 +867,38 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_45", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -743,7 +909,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounding_adjustment", @@ -752,7 +920,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "In Words will be visible once you save the Purchase Order.", @@ -762,7 +932,9 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounded_total", @@ -772,12 +944,16 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break4", "fieldtype": "Column Break", - "oldfieldtype": "Column Break" + "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "grand_total", @@ -787,7 +963,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounding_adjustment", @@ -796,20 +974,26 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -818,7 +1002,9 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advance_paid", @@ -827,19 +1013,25 @@ "no_copy": 1, "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template" + "options": "Payment Terms Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -847,7 +1039,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -856,7 +1050,9 @@ "fieldtype": "Section Break", "label": "Terms and Conditions", "oldfieldtype": "Section Break", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", @@ -865,21 +1061,27 @@ "oldfieldname": "tc_name", "oldfieldtype": "Link", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", "label": "Terms and Conditions", "oldfieldname": "terms", - "oldfieldtype": "Text Editor" + "oldfieldtype": "Text Editor", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", "label": "More Information", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -894,7 +1096,9 @@ "print_hide": 1, "read_only": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "ref_sq", @@ -905,7 +1109,9 @@ "oldfieldname": "ref_sq", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -915,18 +1121,24 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_order_reference", "fieldtype": "Link", "label": "Inter Company Order Reference", "options": "Sales Order", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_74", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.__islocal", @@ -936,7 +1148,9 @@ "label": "% Received", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.__islocal", @@ -946,7 +1160,9 @@ "label": "% Billed", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -956,6 +1172,8 @@ "oldfieldtype": "Column Break", "print_hide": 1, "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -966,7 +1184,9 @@ "oldfieldname": "letter_head", "oldfieldtype": "Select", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -978,11 +1198,15 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_86", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -990,19 +1214,25 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Subscription Section" + "label": "Subscription Section", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1010,7 +1240,9 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1018,11 +1250,15 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_97", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -1031,44 +1267,72 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", - "options": "Tax Category" + "options": "Tax Category", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "supplied_items", "fieldname": "set_reserve_warehouse", "fieldtype": "Link", "label": "Set Reserve Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "tracking_section", "fieldtype": "Section Break", - "label": "Tracking" + "label": "Tracking", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_75", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-04-24 12:13:14.186280", + "modified": "2020-06-13 22:25:47.333850", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1112,6 +1376,12 @@ "read": 1, "role": "Purchase Manager", "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Accounts User" } ], "search_fields": "status, transaction_date, supplier,grand_total", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index f62df20ae1a..c7efb8a1a17 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -71,6 +71,15 @@ class PurchaseOrder(BuyingController): "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="], ["conversion_factor", "="]], "is_child_table": True + }, + "Material Request": { + "ref_dn_field": "material_request", + "compare_fields": [["company", "="]], + }, + "Material Request Item": { + "ref_dn_field": "material_request_item", + "compare_fields": [["project", "="], ["item_code", "="]], + "is_child_table": True } }) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 1712369e60b..813286f7fa4 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -118,7 +118,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(po.get("items")[0].amount, 1400) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) - + def test_add_new_item_in_update_child_qty_rate(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 @@ -144,7 +144,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEquals(len(po.get('items')), 2) self.assertEqual(po.status, 'To Receive and Bill') - + def test_remove_item_in_update_child_qty_rate(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 @@ -185,6 +185,23 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEquals(len(po.get('items')), 1) self.assertEqual(po.status, 'To Receive and Bill') + def test_update_child_qty_rate_perm(self): + po = create_purchase_order(item_code= "_Test Item", qty=4) + + user = 'test@example.com' + test_user = frappe.get_doc('User', user) + test_user.add_roles("Accounts User") + frappe.set_user(user) + + # update qty + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.items[0].name}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) + + # add new item + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) + frappe.set_user("Administrator") + def test_update_qty(self): po = create_purchase_order() @@ -689,7 +706,7 @@ class TestPurchaseOrder(unittest.TestCase): po.save() self.assertEqual(po.schedule_date, add_days(nowdate(), 2)) - + def test_po_optional_blanket_order(self): """ Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1. diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 95db33b0f8f..dfdb487f9e0 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -25,6 +25,7 @@ class RequestforQuotation(BuyingController): self.validate_duplicate_supplier() self.validate_supplier_list() validate_for_items(self) + super(RequestforQuotation, self).set_qty_as_per_stock_uom() self.update_email_id() def validate_duplicate_supplier(self): @@ -278,6 +279,7 @@ def create_rfq_items(sq_doc, supplier, data): "description": data.description, "qty": data.qty, "rate": data.rate, + "conversion_factor": data.conversion_factor if data.conversion_factor else None, "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), "warehouse": data.warehouse or '', "request_for_quotation_item": data.name, diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index dbd9f022789..3de9526c4f2 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -6,12 +6,14 @@ from __future__ import unicode_literals import unittest import frappe -from erpnext.templates.pages.rfq import check_supplier_has_docname_access from frappe.utils import nowdate +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.templates.pages.rfq import check_supplier_has_docname_access +from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation +from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation class TestRequestforQuotation(unittest.TestCase): def test_quote_status(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation rfq = make_request_for_quotation() self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Pending') @@ -31,7 +33,6 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(rfq.get('suppliers')[1].quote_status, 'No Quote') def test_make_supplier_quotation(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation rfq = make_request_for_quotation() sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier) @@ -51,15 +52,13 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(sq1.get('items')[0].qty, 5) def test_make_supplier_quotation_with_special_characters(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation - frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1) supplier = frappe.new_doc("Supplier") supplier.supplier_name = "_Test Supplier '1" supplier.supplier_group = "_Test Supplier Group" supplier.insert() - rfq = make_request_for_quotation(supplier_wt_appos) + rfq = make_request_for_quotation(supplier_data=supplier_wt_appos) sq = make_supplier_quotation(rfq.name, supplier_wt_appos[0].get("supplier")) sq.submit() @@ -76,7 +75,6 @@ class TestRequestforQuotation(unittest.TestCase): frappe.form_dict.name = None def test_make_supplier_quotation_from_portal(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation rfq = make_request_for_quotation() rfq.get('items')[0].rate = 100 rfq.supplier = rfq.suppliers[0].supplier @@ -90,12 +88,34 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(supplier_quotation_doc.get('items')[0].qty, 5) self.assertEqual(supplier_quotation_doc.get('items')[0].amount, 500) + def test_make_multi_uom_supplier_quotation(self): + item_code = "_Test Multi UOM RFQ Item" + if not frappe.db.exists('Item', item_code): + item = make_item(item_code, {'stock_uom': '_Test UOM'}) + row = item.append('uoms', { + 'uom': 'Kg', + 'conversion_factor': 2 + }) + row.db_update() -def make_request_for_quotation(supplier_data=None): + rfq = make_request_for_quotation(item_code="_Test Multi UOM RFQ Item", uom="Kg", conversion_factor=2) + rfq.get('items')[0].rate = 100 + rfq.supplier = rfq.suppliers[0].supplier + + self.assertEqual(rfq.items[0].stock_qty, 10) + + supplier_quotation_name = create_supplier_quotation(rfq) + supplier_quotation = frappe.get_doc('Supplier Quotation', supplier_quotation_name) + + self.assertEqual(supplier_quotation.items[0].qty, 5) + self.assertEqual(supplier_quotation.items[0].stock_qty, 10) + +def make_request_for_quotation(**args): """ :param supplier_data: List containing supplier data """ - supplier_data = supplier_data if supplier_data else get_supplier_data() + args = frappe._dict(args) + supplier_data = args.get("supplier_data") if args.get("supplier_data") else get_supplier_data() rfq = frappe.new_doc('Request for Quotation') rfq.transaction_date = nowdate() rfq.status = 'Draft' @@ -106,11 +126,13 @@ def make_request_for_quotation(supplier_data=None): rfq.append('suppliers', data) rfq.append("items", { - "item_code": "_Test Item", + "item_code": args.item_code or "_Test Item", "description": "_Test Item", - "uom": "_Test UOM", - "qty": 5, - "warehouse": "_Test Warehouse - _TC", + "uom": args.uom or "_Test UOM", + "stock_uom": args.stock_uom or "_Test UOM", + "qty": args.qty or 5, + "conversion_factor": args.conversion_factor or 1.0, + "warehouse": args.warehouse or "_Test Warehouse - _TC", "schedule_date": nowdate() }) diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index 0159df962ec..408f49f5233 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2016-02-25 08:04:02.452958", "doctype": "DocType", @@ -9,6 +10,7 @@ "supplier_part_no", "column_break_3", "item_name", + "schedule_date", "section_break_5", "description", "item_group", @@ -18,9 +20,11 @@ "image_view", "quantity", "qty", + "stock_uom", "col_break2", - "schedule_date", "uom", + "conversion_factor", + "stock_qty", "warehouse_and_reference", "warehouse", "project_name", @@ -33,7 +37,7 @@ "fields": [ { "bold": 1, - "columns": 3, + "columns": 2, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -98,7 +102,7 @@ { "fieldname": "quantity", "fieldtype": "Section Break", - "label": "Quantity" + "label": "Quantity & Stock" }, { "bold": 1, @@ -129,12 +133,12 @@ { "fieldname": "uom", "fieldtype": "Link", + "in_list_view": 1, "label": "UOM", "oldfieldname": "uom", "oldfieldtype": "Link", "options": "UOM", "print_width": "100px", - "read_only": 1, "reqd": 1, "width": "100px" }, @@ -144,7 +148,7 @@ "label": "Warehouse and Reference" }, { - "columns": 3, + "columns": 2, "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -202,6 +206,7 @@ }, { "allow_on_submit": 1, + "default": "0", "fieldname": "page_break", "fieldtype": "Check", "label": "Page Break", @@ -219,10 +224,36 @@ { "fieldname": "section_break_23", "fieldtype": "Section Break" + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "UOM Conversion Factor", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Qty as per Stock UOM", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "istable": 1, - "modified": "2019-05-01 17:50:23.703801", + "links": [], + "modified": "2020-06-12 19:10:36.333441", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Item", diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 39668795cba..88a865f0f85 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import flt def execute(filters=None): columns = get_columns(filters) @@ -54,15 +55,16 @@ def get_columns(filters): "width": 140 }, { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 200 + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150 }, { "label": _("Quantity"), "fieldname": "quantity", - "fieldtype": "Int", + "fieldtype": "Float", "width": 140 }, { @@ -118,7 +120,7 @@ def get_columns(filters): }, { "label": _("Purchase Order Amount(Company Currency)"), - "fieldname": "purchase_order_amt_usd", + "fieldname": "purchase_order_amt_in_company_currency", "fieldtype": "Float", "width": 140 }, @@ -175,17 +177,17 @@ def get_data(filters): "requesting_site": po.warehouse, "requestor": po.owner, "material_request_no": po.material_request, - "description": po.description, - "quantity": po.qty, + "item_code": po.item_code, + "quantity": flt(po.qty), "unit_of_measurement": po.stock_uom, "status": po.status, "purchase_order_date": po.transaction_date, "purchase_order": po.parent, "supplier": po.supplier, - "estimated_cost": mr_record.get('amount'), - "actual_cost": pi_records.get(po.name), - "purchase_order_amt": po.amount, - "purchase_order_amt_in_company_currency": po.base_amount, + "estimated_cost": flt(mr_record.get('amount')), + "actual_cost": flt(pi_records.get(po.name)), + "purchase_order_amt": flt(po.amount), + "purchase_order_amt_in_company_currency": flt(po.base_amount), "expected_delivery_date": po.schedule_date, "actual_delivery_date": pr_records.get(po.name) } @@ -198,9 +200,14 @@ def get_mapped_mr_details(conditions): SELECT par.transaction_date, par.per_ordered, + par.owner, child.name, child.parent, - child.amount + child.amount, + child.qty, + child.item_code, + child.uom, + par.status FROM `tabMaterial Request` par, `tabMaterial Request Item` child WHERE par.per_ordered>=0 @@ -217,7 +224,15 @@ def get_mapped_mr_details(conditions): procurement_record_details = dict( material_request_date=record.transaction_date, material_request_no=record.parent, - estimated_cost=record.amount + requestor=record.owner, + item_code=record.item_code, + estimated_cost=flt(record.amount), + quantity=flt(record.qty), + unit_of_measurement=record.uom, + status=record.status, + actual_cost=0, + purchase_order_amt=0, + purchase_order_amt_in_company_currency=0 ) procurement_record_against_mr.append(procurement_record_details) return mr_records, procurement_record_against_mr @@ -259,7 +274,7 @@ def get_po_entries(conditions): child.warehouse, child.material_request, child.material_request_item, - child.description, + child.item_code, child.stock_uom, child.qty, child.amount, diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 06da3368927..44ab767c0a9 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -37,8 +37,6 @@ class TestProcurementTracker(unittest.TestCase): po = make_purchase_order(mr.name) po.supplier = "_Test Supplier" po.get("items")[0].cost_center = "Main - _TPC" - po.items[0].rate = 500.0 - po.save() po.submit() pr = make_purchase_receipt(po.name) pr.get("items")[0].cost_center = "Main - _TPC" @@ -55,7 +53,7 @@ class TestProcurementTracker(unittest.TestCase): "requesting_site": "_Test Procurement Warehouse - _TPC", "requestor": "Administrator", "material_request_no": mr.name, - "description": '_Test Item 1', + "item_code": '_Test Item', "quantity": 10.0, "unit_of_measurement": "_Test UOM", "status": "To Bill", @@ -63,7 +61,7 @@ class TestProcurementTracker(unittest.TestCase): "purchase_order": po.name, "supplier": "_Test Supplier", "estimated_cost": 0.0, - "actual_cost": None, + "actual_cost": 0.0, "purchase_order_amt": po.net_total, "purchase_order_amt_in_company_currency": po.base_net_total, "expected_delivery_date": date_obj, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index eecb143d556..f54b593022c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1137,8 +1137,8 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.item_name = item.item_name child_item.description = item.description child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date + child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1157,8 +1157,8 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.item_name = item.item_name child_item.description = item.description child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date + child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item @@ -1190,6 +1190,26 @@ def check_and_delete_children(parent, data): @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): + def check_permissions(doc, perm_type='create'): + try: + doc.check_permission(perm_type) + except: + action = "add" if perm_type == 'create' else "update" + frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions")) + + def get_new_child_item(item_row): + if parent_doctype == "Sales Order": + return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + if parent_doctype == "Purchase Order": + return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + + def validate_quantity(child_item, d): + if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): + frappe.throw(_("Cannot set quantity less than delivered quantity")) + + if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty): + frappe.throw(_("Cannot set quantity less than received quantity")) + data = json.loads(trans_items) sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] @@ -1201,20 +1221,29 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil new_child_flag = False if not d.get("docname"): new_child_flag = True - if parent_doctype == "Sales Order": - child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) - if parent_doctype == "Purchase Order": - child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) + check_permissions(parent, 'create') + child_item = get_new_child_item(d) else: + check_permissions(parent, 'write') child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) - if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")): + + prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) + prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) + prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor")) + + if parent_doctype == 'Sales Order': + prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date") + elif parent_doctype == 'Purchase Order': + prev_date, new_date = child_item.get("schedule_date"), d.get("schedule_date") + + rate_unchanged = prev_rate == new_rate + qty_unchanged = prev_qty == new_qty + conversion_factor_unchanged = prev_con_fac == new_con_fac + date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc + if rate_unchanged and qty_unchanged and conversion_factor_unchanged and date_unchanged: continue - if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): - frappe.throw(_("Cannot set quantity less than delivered quantity")) - - if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty): - frappe.throw(_("Cannot set quantity less than received quantity")) + validate_quantity(child_item, d) child_item.qty = flt(d.get("qty")) precision = child_item.precision("rate") or 2 @@ -1225,6 +1254,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil else: child_item.rate = flt(d.get("rate")) + if d.get("conversion_factor"): + if child_item.stock_uom == child_item.uom: + child_item.conversion_factor = 1 + else: + child_item.conversion_factor = flt(d.get('conversion_factor')) + + if d.get("delivery_date") and parent_doctype == 'Sales Order': + child_item.delivery_date = d.get('delivery_date') + + if d.get("schedule_date") and parent_doctype == 'Purchase Order': + child_item.schedule_date = d.get('schedule_date') + if flt(child_item.price_list_rate): if flt(child_item.rate) > flt(child_item.price_list_rate): # if rate is greater than price_list_rate, set margin diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 608e537e1bb..89b48f07ee8 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -349,7 +349,7 @@ class BuyingController(StockController): }) if not rm.rate: - rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse, + rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse, self.doctype, self.name, currency=self.company_currency, company=self.company) rm.amount = qty * flt(rm.rate) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 29f8dd57026..1f95e004244 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -70,7 +70,7 @@ def validate_item_variant_attributes(item, args=None): else: attributes_list = attribute_values.get(attribute.lower(), []) - validate_item_attribute_value(attributes_list, attribute, value, item.name) + validate_item_attribute_value(attributes_list, attribute, value, item.name, from_variant=True) def validate_is_incremental(numeric_attribute, attribute, value, item): from_range = numeric_attribute.from_range @@ -93,13 +93,20 @@ def validate_is_incremental(numeric_attribute, attribute, value, item): .format(attribute, from_range, to_range, increment, item), InvalidItemAttributeValueError, title=_('Invalid Attribute')) -def validate_item_attribute_value(attributes_list, attribute, attribute_value, item): +def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True): allow_rename_attribute_value = frappe.db.get_single_value('Item Variant Settings', 'allow_rename_attribute_value') if allow_rename_attribute_value: pass elif attribute_value not in attributes_list: - frappe.throw(_("The value {0} is already assigned to an exisiting Item {2}.").format( - attribute_value, attribute, item), InvalidItemAttributeValueError, title=_('Rename Not Allowed')) + if from_variant: + frappe.throw(_("{0} is not a valid Value for Attribute {1} of Item {2}.").format( + frappe.bold(attribute_value), frappe.bold(attribute), frappe.bold(item)), InvalidItemAttributeValueError, title=_("Invalid Value")) + else: + msg = _("The value {0} is already assigned to an existing Item {1}.").format( + frappe.bold(attribute_value), frappe.bold(item)) + msg += "
" + _("To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings.").format(frappe.bold("Allow Rename Attribute Value")) + + frappe.throw(msg, InvalidItemAttributeValueError, title=_('Edit Not Allowed')) def get_attribute_values(item): if not frappe.flags.attribute_values: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 83139b1fd5b..d098c1cfd54 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -19,7 +19,8 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass class StockController(AccountsController): def validate(self): super(StockController, self).validate() - self.validate_inspection() + if not self.get('is_return'): + self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() @@ -228,7 +229,9 @@ class StockController(AccountsController): def check_expense_account(self, item): if not item.get("expense_account"): - frappe.throw(_("Expense Account not set for Item {0}. Please set an Expense Account for the item in the Items table").format(item.item_code)) + frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \ + Account in the Items table").format(item.idx, frappe.bold(item.item_code)), + title=_("Expense Account Missing")) else: is_expense_account = frappe.db.get_value("Account", diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 6fef0c46437..f5f8b4efb34 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -6,6 +6,7 @@ "creation": "2013-04-10 11:45:37", "doctype": "DocType", "document_type": "Document", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "organization_lead", @@ -448,7 +449,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2020-05-11 20:27:45.868960", + "modified": "2020-06-18 14:39:41.835416", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -508,8 +509,10 @@ } ], "search_fields": "lead_name,lead_owner,status", + "sender_field": "email_id", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "subject_field": "title", "title_field": "title" } \ No newline at end of file diff --git a/erpnext/crm/doctype/opportunity/opportunity_dashboard.py b/erpnext/crm/doctype/opportunity/opportunity_dashboard.py index 9ed616afd23..68f0104fd6c 100644 --- a/erpnext/crm/doctype/opportunity/opportunity_dashboard.py +++ b/erpnext/crm/doctype/opportunity/opportunity_dashboard.py @@ -3,11 +3,7 @@ from frappe import _ def get_data(): return { - 'fieldname': 'prevdoc_docname', - 'non_standard_fieldnames': { - 'Supplier Quotation': 'opportunity', - 'Quotation': 'opportunity' - }, + 'fieldname': 'opportunity', 'transactions': [ { 'items': ['Quotation', 'Supplier Quotation'] diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index 2601c14b4d6..0a00dca2808 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -30,24 +30,32 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "mandatory_depends_on": "eval:doc.twitter ==1" + "mandatory_depends_on": "eval:doc.twitter ==1", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image" + "label": "Image", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "twitter", "fieldtype": "Check", - "label": "Twitter" + "label": "Twitter", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "linkedin", "fieldtype": "Check", - "label": "LinkedIn" + "label": "LinkedIn", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -56,13 +64,17 @@ "no_copy": 1, "options": "Social Media Post", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", - "label": "Twitter" + "label": "Twitter", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -70,7 +82,9 @@ "fieldtype": "Select", "label": "Post Status", "options": "\nScheduled\nPosted\nError", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -78,7 +92,9 @@ "fieldtype": "Data", "hidden": 1, "label": "Twitter Post Id", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -86,68 +102,89 @@ "fieldtype": "Data", "hidden": 1, "label": "LinkedIn Post Id", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "campaign_name", "fieldtype": "Link", "in_list_view": 1, "label": "Campaign", - "options": "Campaign" + "options": "Campaign", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_6", "fieldtype": "Column Break", - "label": "Share On" + "label": "Share On", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_14", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tweet_preview", - "fieldtype": "HTML" + "fieldtype": "HTML", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", - "label": "LinkedIn" + "label": "LinkedIn", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "attachments_section", "fieldtype": "Section Break", - "label": "Attachments" + "label": "Attachments", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "linkedin_post", "fieldtype": "Text", "label": "Post", - "mandatory_depends_on": "eval:doc.linkedin ==1" + "mandatory_depends_on": "eval:doc.linkedin ==1", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "scheduled_time", "fieldtype": "Datetime", "label": "Scheduled Time", - "read_only_depends_on": "eval:doc.post_status == \"Posted\"" + "read_only_depends_on": "eval:doc.post_status == \"Posted\"", + "show_days": 1, + "show_seconds": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-21 15:10:04.953713", + "modified": "2020-06-14 10:31:33.961381", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -157,6 +194,35 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/education/doctype/fee_structure/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js index 7606565fadb..f09d2efcb9e 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.js +++ b/erpnext/education/doctype/fee_structure/fee_structure.js @@ -9,6 +9,14 @@ frappe.ui.form.on('Fee Structure', { }, onload: function(frm) { + frm.set_query("academic_term", function() { + return { + "filters": { + "academic_year": frm.doc.academic_year + } + }; + }); + frm.set_query("receivable_account", function(doc) { return { filters: { diff --git a/erpnext/education/doctype/fee_structure/fee_structure.json b/erpnext/education/doctype/fee_structure/fee_structure.json index 8ff6851d90a..67e46372f82 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.json +++ b/erpnext/education/doctype/fee_structure/fee_structure.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", @@ -11,8 +12,8 @@ "program", "student_category", "column_break_2", - "academic_term", "academic_year", + "academic_term", "section_break_4", "components", "section_break_6", @@ -157,7 +158,8 @@ ], "icon": "fa fa-flag", "is_submittable": 1, - "modified": "2019-05-26 09:04:17.765758", + "links": [], + "modified": "2020-06-16 15:34:57.295010", "modified_by": "Administrator", "module": "Education", "name": "Fee Structure", diff --git a/erpnext/education/doctype/program_course/program_course.json b/erpnext/education/doctype/program_course/program_course.json index a24e88a8611..940358e4e9b 100644 --- a/erpnext/education/doctype/program_course/program_course.json +++ b/erpnext/education/doctype/program_course/program_course.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2015-09-07 14:37:01.886859", "doctype": "DocType", "editable_grid": 1, @@ -16,26 +17,33 @@ "in_list_view": 1, "label": "Course", "options": "Course", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, - { + { + "fetch_from": "course.course_name", "fieldname": "course_name", "fieldtype": "Data", "in_list_view": 1, "label": "Course Name", - "fetch_from": "course.course_name", - "read_only":1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "required", "fieldtype": "Check", "in_list_view": 1, - "label": "Mandatory" + "label": "Mandatory", + "show_days": 1, + "show_seconds": 1 } ], "istable": 1, - "modified": "2019-06-12 12:42:12.845972", + "links": [], + "modified": "2020-06-09 18:56:10.213241", "modified_by": "Administrator", "module": "Education", "name": "Program Course", @@ -45,4 +53,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/education/doctype/student_admission/student_admission.json b/erpnext/education/doctype/student_admission/student_admission.json index b3c10d43316..1096888d4d2 100644 --- a/erpnext/education/doctype/student_admission/student_admission.json +++ b/erpnext/education/doctype/student_admission/student_admission.json @@ -1,398 +1,119 @@ { - "allow_copy": 0, - "allow_guest_to_view": 1, - "allow_import": 0, - "allow_rename": 1, - "autoname": "", - "beta": 0, - "creation": "2016-09-13 03:05:27.154713", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "allow_guest_to_view": 1, + "allow_rename": 1, + "creation": "2016-09-13 03:05:27.154713", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "route", + "column_break_3", + "academic_year", + "admission_start_date", + "admission_end_date", + "published", + "enable_admission_application", + "section_break_5", + "program_details", + "introduction" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "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": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "route", - "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": "Route", - "length": 0, - "no_copy": 1, - "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, + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", + "no_copy": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "application_form_route", - "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": "Application Form Route", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "academic_year", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Academic Year", + "no_copy": 1, + "options": "Academic Year", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Academic Year", - "length": 0, - "no_copy": 1, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "admission_start_date", + "fieldtype": "Date", + "label": "Admission Start Date", + "no_copy": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "admission_start_date", - "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": "Admission Start Date", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "admission_end_date", + "fieldtype": "Date", + "label": "Admission End Date", + "no_copy": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "admission_end_date", - "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": "Admission End Date", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Publish on website" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "published", - "fieldtype": "Check", - "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": "Publish on website", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Eligibility and Details" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "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": "Eligibility and Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "program_details", + "fieldtype": "Table", + "label": "Eligibility and Details", + "options": "Student Admission Program" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program_details", - "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, - "label": "Eligibility and Details", - "length": 0, - "no_copy": 0, - "options": "Student Admission Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "introduction", + "fieldtype": "Text Editor", + "label": "Introduction" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "introduction", - "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": "Introduction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "0", + "fieldname": "enable_admission_application", + "fieldtype": "Check", + "label": "Enable Admission Application" } - ], - "has_web_view": 1, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_published_field": "published", - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-11-10 18:57:34.570376", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Admission", - "name_case": "", - "owner": "Administrator", + ], + "has_web_view": 1, + "is_published_field": "published", + "links": [], + "modified": "2020-06-15 20:18:38.591626", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Admission", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 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": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "route": "admissions", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0 + ], + "restrict_to_domain": "Education", + "route": "admissions", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/education/doctype/student_admission/templates/student_admission.html b/erpnext/education/doctype/student_admission/templates/student_admission.html index 25afaca84dc..e5a9ead31ed 100644 --- a/erpnext/education/doctype/student_admission/templates/student_admission.html +++ b/erpnext/education/doctype/student_admission/templates/student_admission.html @@ -43,8 +43,8 @@ Program/Std. - Minumum Age(DOB) - Maximum Age(DOB) + Minumum Age + Maximum Age Application Fee @@ -52,8 +52,8 @@ {% for row in program_details %} {{ row.program }} - {{ row.minimum_age }} - {{ row.maximum_age }} + {{ row.min_age }} + {{ row.max_age }} {{ row.application_fee }} {% endfor %} @@ -61,12 +61,11 @@ {% endif %} - - {%- if application_form_route -%} + {%- if doc.enable_admission_application -%}

+ href='/student-applicant?new=1&student_admission={{doc.name}}'> {{ _("Apply Now") }}

{% endif %} diff --git a/erpnext/education/doctype/student_admission/test_student_admission.js b/erpnext/education/doctype/student_admission/test_student_admission.js index ed794b2482e..3a0bb0b2f23 100644 --- a/erpnext/education/doctype/student_admission/test_student_admission.js +++ b/erpnext/education/doctype/student_admission/test_student_admission.js @@ -11,7 +11,7 @@ QUnit.test('Test: Student Admission', function(assert) { {admission_start_date: '2016-04-20'}, {admission_end_date: '2016-05-31'}, {title: '2016-17 Admissions'}, - {application_form_route: 'student-applicant'}, + {enable_admission_application: 1}, {introduction: 'Test intro'}, {program_details: [ [ @@ -28,7 +28,7 @@ QUnit.test('Test: Student Admission', function(assert) { assert.ok(cur_frm.doc.admission_start_date == '2016-04-20'); assert.ok(cur_frm.doc.admission_end_date == '2016-05-31'); assert.ok(cur_frm.doc.title == '2016-17 Admissions'); - assert.ok(cur_frm.doc.application_form_route == 'student-applicant'); + assert.ok(cur_frm.doc.enable_admission_application == 1); assert.ok(cur_frm.doc.introduction == 'Test intro'); assert.ok(cur_frm.doc.program_details[0].program == 'Standard Test', 'Program correctly selected'); assert.ok(cur_frm.doc.program_details[0].application_fee == 1000); diff --git a/erpnext/education/doctype/student_admission_program/student_admission_program.json b/erpnext/education/doctype/student_admission_program/student_admission_program.json index 97b1bba4217..e9f041e101f 100644 --- a/erpnext/education/doctype/student_admission_program/student_admission_program.json +++ b/erpnext/education/doctype/student_admission_program/student_admission_program.json @@ -1,237 +1,77 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-09-15 12:59:43.207923", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-09-15 12:59:43.207923", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "program", + "min_age", + "max_age", + "column_break_4", + "application_fee", + "applicant_naming_series" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "program", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Program", + "options": "Program", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "minimum_age", - "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": "Minimum Age", - "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_4", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maximum_age", - "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": "Maximum Age", - "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": "application_fee", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Application Fee", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "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": "applicant_naming_series", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Naming Series (for Student Applicant)", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "application_fee", - "fieldtype": "Currency", - "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": "Application Fee", - "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": "min_age", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Minimum Age", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "applicant_naming_series", - "fieldtype": "Data", - "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": "Naming Series (for Student Applicant)", - "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": "max_age", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Maximum Age", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-04 03:37:17.408427", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Admission Program", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-06-10 23:06:30.037404", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Admission Program", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/education/doctype/student_applicant/student_applicant.py b/erpnext/education/doctype/student_applicant/student_applicant.py index ab947807dd8..211348201e3 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.py +++ b/erpnext/education/doctype/student_applicant/student_applicant.py @@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate +from frappe.utils import getdate, add_years, nowdate, date_diff class StudentApplicant(Document): def autoname(self): @@ -31,6 +31,7 @@ class StudentApplicant(Document): def validate(self): self.validate_dates() self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + if self.student_admission and self.program and self.date_of_birth: self.validation_from_student_admission() @@ -48,16 +49,16 @@ class StudentApplicant(Document): frappe.throw(_("Please select Student Admission which is mandatory for the paid student applicant")) def validation_from_student_admission(self): + student_admission = get_student_admission_data(self.student_admission, self.program) - # different validation for minimum and maximum age so that either min/max can also work independently. - if student_admission and student_admission.minimum_age and \ - getdate(student_admission.minimum_age) < getdate(self.date_of_birth): - frappe.throw(_("Not eligible for the admission in this program as per DOB")) + if student_admission and student_admission.min_age and \ + date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.min_age)) < 0: + frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth")) - if student_admission and student_admission.maximum_age and \ - getdate(student_admission.maximum_age) > getdate(self.date_of_birth): - frappe.throw(_("Not eligible for the admission in this program as per DOB")) + if student_admission and student_admission.max_age and \ + date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.max_age)) > 0: + frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth")) def on_payment_authorized(self, *args, **kwargs): @@ -65,10 +66,12 @@ class StudentApplicant(Document): def get_student_admission_data(student_admission, program): + student_admission = frappe.db.sql("""select sa.admission_start_date, sa.admission_end_date, - sap.program, sap.minimum_age, sap.maximum_age, sap.applicant_naming_series + sap.program, sap.min_age, sap.max_age, sap.applicant_naming_series from `tabStudent Admission` sa, `tabStudent Admission Program` sap where sa.name = sap.parent and sa.name = %s and sap.program = %s""", (student_admission, program), as_dict=1) + if student_admission: return student_admission[0] else: diff --git a/erpnext/education/web_form/student_applicant/student_applicant.json b/erpnext/education/web_form/student_applicant/student_applicant.json index b1ad754c327..1810f07a054 100644 --- a/erpnext/education/web_form/student_applicant/student_applicant.json +++ b/erpnext/education/web_form/student_applicant/student_applicant.json @@ -1,200 +1,248 @@ { - "accept_payment": 0, - "allow_comments": 0, - "allow_delete": 0, - "allow_edit": 1, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, - "creation": "2016-09-22 13:10:10.792735", - "doc_type": "Student Applicant", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "is_standard": 1, - "login_required": 1, - "max_attachment_size": 0, - "modified": "2017-02-21 05:44:46.022738", - "modified_by": "Administrator", - "module": "Education", - "name": "student-applicant", - "owner": "Administrator", - "payment_button_label": "Buy Now", - "published": 1, - "route": "student-applicant", - "show_sidebar": 1, - "sidebar_items": [], - "success_url": "/student-applicant", - "title": "Student Applicant", + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "creation": "2016-09-22 13:10:10.792735", + "doc_type": "Student Applicant", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "is_standard": 1, + "login_required": 1, + "max_attachment_size": 0, + "modified": "2020-06-11 22:53:45.875310", + "modified_by": "Administrator", + "module": "Education", + "name": "student-applicant", + "owner": "Administrator", + "payment_button_label": "Buy Now", + "published": 1, + "route": "student-applicant", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 1, + "sidebar_items": [], + "success_url": "/student-applicant", + "title": "Student Applicant", "web_form_fields": [ { - "fieldname": "first_name", - "fieldtype": "Data", - "hidden": 0, - "label": "First Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "first_name", + "fieldtype": "Data", + "hidden": 0, + "label": "First Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "middle_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Middle Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "middle_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Middle Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "last_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Last Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "last_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Last Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "image", - "fieldtype": "Data", - "hidden": 0, - "label": "Image", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "image", + "fieldtype": "Data", + "hidden": 0, + "label": "Image", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "label": "Program", - "max_length": 0, - "max_value": 0, - "options": "Program", - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "program", + "fieldtype": "Link", + "hidden": 0, + "label": "Program", + "max_length": 0, + "max_value": 0, + "options": "Program", + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "label": "Academic Year", - "max_length": 0, - "max_value": 0, - "options": "Academic Year", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "academic_year", + "fieldtype": "Link", + "hidden": 0, + "label": "Academic Year", + "max_length": 0, + "max_value": 0, + "options": "Academic Year", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "date_of_birth", - "fieldtype": "Date", - "hidden": 0, - "label": "Date of Birth", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "date_of_birth", + "fieldtype": "Date", + "hidden": 0, + "label": "Date of Birth", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "blood_group", - "fieldtype": "Select", - "hidden": 0, - "label": "Blood Group", - "max_length": 0, - "max_value": 0, - "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "blood_group", + "fieldtype": "Select", + "hidden": 0, + "label": "Blood Group", + "max_length": 0, + "max_value": 0, + "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "student_email_id", - "fieldtype": "Data", - "hidden": 0, - "label": "Student Email ID", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "student_email_id", + "fieldtype": "Data", + "hidden": 0, + "label": "Student Email ID", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "student_mobile_number", - "fieldtype": "Data", - "hidden": 0, - "label": "Student Mobile Number", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "student_mobile_number", + "fieldtype": "Data", + "hidden": 0, + "label": "Student Mobile Number", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "default": "INDIAN", - "fieldname": "nationality", - "fieldtype": "Data", - "hidden": 0, - "label": "Nationality", - "max_length": 0, - "max_value": 0, - "options": "", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "default": "INDIAN", + "fieldname": "nationality", + "fieldtype": "Data", + "hidden": 0, + "label": "Nationality", + "max_length": 0, + "max_value": 0, + "options": "", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "address_line_1", - "fieldtype": "Data", - "hidden": 0, - "label": "Address Line 1", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "address_line_1", + "fieldtype": "Data", + "hidden": 0, + "label": "Address Line 1", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "address_line_2", - "fieldtype": "Data", - "hidden": 0, - "label": "Address Line 2", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "address_line_2", + "fieldtype": "Data", + "hidden": 0, + "label": "Address Line 2", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "pincode", - "fieldtype": "Data", - "hidden": 0, - "label": "Pincode", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "pincode", + "fieldtype": "Data", + "hidden": 0, + "label": "Pincode", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "guardians", - "fieldtype": "Table", - "hidden": 0, - "label": "Guardians", - "max_length": 0, - "max_value": 0, - "options": "Student Guardian", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "guardians", + "fieldtype": "Table", + "hidden": 0, + "label": "Guardians", + "max_length": 0, + "max_value": 0, + "options": "Student Guardian", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "siblings", - "fieldtype": "Table", - "hidden": 0, - "label": "Siblings", - "max_length": 0, - "max_value": 0, - "options": "Student Sibling", - "read_only": 0, - "reqd": 0 + "allow_read_on_all_link_options": 0, + "fieldname": "siblings", + "fieldtype": "Table", + "hidden": 0, + "label": "Siblings", + "max_length": 0, + "max_value": 0, + "options": "Student Sibling", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "student_admission", + "fieldtype": "Link", + "hidden": 0, + "label": "Student Admission", + "max_length": 0, + "max_value": 0, + "options": "Student Admission", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 } ] } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index 7046038fb24..d59f9092983 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -241,14 +241,17 @@ def get_order_taxes(shopify_order, shopify_settings): return taxes def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings): + """Shipping lines represents the shipping details, + each such shipping detail consists of a list of tax_lines""" for shipping_charge in shipping_lines: - taxes.append({ - "charge_type": _("Actual"), - "account_head": get_tax_account_head(shipping_charge), - "description": shipping_charge["title"], - "tax_amount": shipping_charge["price"], - "cost_center": shopify_settings.cost_center - }) + for tax in shipping_charge.get("tax_lines"): + taxes.append({ + "charge_type": _("Actual"), + "account_head": get_tax_account_head(tax), + "description": tax["title"], + "tax_amount": tax["price"], + "cost_center": shopify_settings.cost_center + }) return taxes diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 1b0c9f60b6e..6dedaa8c530 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -73,10 +73,16 @@ def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name if customer_exists: frappe.rename_doc("Customer", old_name, customer_name) - 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) + for address_type in ("Billing", "Shipping",): + try: + address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type}) + rename_address(address, customer) + except ( + frappe.DoesNotExistError, + frappe.DuplicateEntryError, + frappe.ValidationError, + ): + pass else: create_address(raw_billing_data, customer, "Billing") create_address(raw_shipping_data, customer, "Shipping") diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py index 64c3b2d2730..25ffd281099 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py @@ -8,6 +8,7 @@ import json from frappe import _ from frappe.model.document import Document from frappe.utils import get_request_session +from requests.exceptions import HTTPError from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from erpnext.erpnext_integrations.utils import get_webhook_address from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log @@ -29,19 +30,24 @@ class ShopifySettings(Document): webhooks = ["orders/create", "orders/paid", "orders/fulfilled"] # url = get_shopify_url('admin/webhooks.json', self) created_webhooks = [d.method for d in self.webhooks] - url = get_shopify_url('admin/api/2019-04/webhooks.json', self) + url = get_shopify_url('admin/api/2020-04/webhooks.json', self) for method in webhooks: session = get_request_session() try: - d = session.post(url, data=json.dumps({ + res = session.post(url, data=json.dumps({ "webhook": { "topic": method, "address": get_webhook_address(connector_name='shopify_connection', method='store_request_data'), "format": "json" } }), headers=get_header(self)) - d.raise_for_status() - self.update_webhook_table(method, d.json()) + res.raise_for_status() + self.update_webhook_table(method, res.json()) + + except HTTPError as e: + error_message = res.json().get('errors', e) + make_shopify_log(status="Warning", exception=error_message, rollback=True) + except Exception as e: make_shopify_log(status="Warning", exception=e, rollback=True) @@ -50,13 +56,18 @@ class ShopifySettings(Document): deleted_webhooks = [] for d in self.webhooks: - url = get_shopify_url('admin/api/2019-04/webhooks/{0}.json'.format(d.webhook_id), self) + url = get_shopify_url('admin/api/2020-04/webhooks/{0}.json'.format(d.webhook_id), self) try: res = session.delete(url, headers=get_header(self)) res.raise_for_status() deleted_webhooks.append(d) + + except HTTPError as e: + error_message = res.json().get('errors', e) + make_shopify_log(status="Warning", exception=error_message, rollback=True) + except Exception as e: - frappe.log_error(message=frappe.get_traceback(), title=e) + frappe.log_error(message=e, title='Shopify Webhooks Issue') for d in deleted_webhooks: self.remove(d) @@ -125,4 +136,3 @@ def setup_custom_fields(): } create_custom_fields(custom_fields) - diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py index bde101123db..f9f0bb3cecc 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py @@ -8,7 +8,7 @@ from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings impo shopify_variants_attr_list = ["option1", "option2", "option3"] def sync_item_from_shopify(shopify_settings, item): - url = get_shopify_url("admin/api/2019-04/products/{0}.json".format(item.get("product_id")), shopify_settings) + url = get_shopify_url("admin/api/2020-04/products/{0}.json".format(item.get("product_id")), shopify_settings) session = get_request_session() try: diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js index d84c8234efa..fd16d1e84aa 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js @@ -1,7 +1,9 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Tally Migration', { +frappe.provide("erpnext.tally_migration"); + +frappe.ui.form.on("Tally Migration", { onload: function (frm) { let reload_status = true; frappe.realtime.on("tally_migration_progress_update", function (data) { @@ -35,7 +37,17 @@ frappe.ui.form.on('Tally Migration', { } }); }, + refresh: function (frm) { + frm.trigger("show_logs_preview"); + erpnext.tally_migration.failed_import_log = JSON.parse(frm.doc.failed_import_log); + erpnext.tally_migration.fixed_errors_log = JSON.parse(frm.doc.fixed_errors_log); + + ["default_round_off_account", "default_warehouse", "default_cost_center"].forEach(account => { + frm.toggle_reqd(account, frm.doc.is_master_data_imported === 1) + frm.toggle_enable(account, frm.doc.is_day_book_data_processed != 1) + }) + if (frm.doc.master_data && !frm.doc.is_master_data_imported) { if (frm.doc.is_master_data_processed) { if (frm.doc.status != "Importing Master Data") { @@ -47,6 +59,7 @@ frappe.ui.form.on('Tally Migration', { } } } + if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) { if (frm.doc.is_day_book_data_processed) { if (frm.doc.status != "Importing Day Book Data") { @@ -59,6 +72,17 @@ frappe.ui.form.on('Tally Migration', { } } }, + + erpnext_company: function (frm) { + frappe.db.exists("Company", frm.doc.erpnext_company).then(exists => { + if (exists) { + frappe.msgprint( + __("Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts", [frm.doc.erpnext_company]), + ); + } + }); + }, + add_button: function (frm, label, method) { frm.add_custom_button( label, @@ -71,5 +95,255 @@ frappe.ui.form.on('Tally Migration', { frm.reload_doc(); } ); + }, + + render_html_table(frm, shown_logs, hidden_logs, field) { + if (shown_logs && shown_logs.length > 0) { + frm.toggle_display(field, true); + } else { + frm.toggle_display(field, false); + return + } + let rows = erpnext.tally_migration.get_html_rows(shown_logs, field); + let rows_head, table_caption; + + let table_footer = (hidden_logs && (hidden_logs.length > 0)) ? ` + And ${hidden_logs.length} more others + `: ""; + + if (field === "fixed_error_log_preview") { + rows_head = `${__("Meta Data")} + ${__("Unresolve")}` + table_caption = "Resolved Issues" + } else { + rows_head = `${__("Error Message")} + ${__("Create")}` + table_caption = "Error Log" + } + + frm.get_field(field).$wrapper.html(` + + + + + + ${rows_head} + + ${rows} + ${table_footer} +
${table_caption}
${__("#")}${__("DocType")}
+ `); + }, + + show_error_summary(frm) { + let summary = erpnext.tally_migration.failed_import_log.reduce((summary, row) => { + if (row.doc) { + if (summary[row.doc.doctype]) { + summary[row.doc.doctype] += 1; + } else { + summary[row.doc.doctype] = 1; + } + } + return summary + }, {}); + console.table(summary); + }, + + show_logs_preview(frm) { + let empty = "[]"; + let import_log = frm.doc.failed_import_log || empty; + let completed_log = frm.doc.fixed_errors_log || empty; + let render_section = !(import_log === completed_log && import_log === empty); + + frm.toggle_display("import_log_section", render_section); + if (render_section) { + frm.trigger("show_error_summary"); + frm.trigger("show_errored_import_log"); + frm.trigger("show_fixed_errors_log"); + } + }, + + show_errored_import_log(frm) { + let import_log = erpnext.tally_migration.failed_import_log; + let logs = import_log.slice(0, 20); + let hidden_logs = import_log.slice(20); + + frm.events.render_html_table(frm, logs, hidden_logs, "failed_import_preview"); + }, + + show_fixed_errors_log(frm) { + let completed_log = erpnext.tally_migration.fixed_errors_log; + let logs = completed_log.slice(0, 20); + let hidden_logs = completed_log.slice(20); + + frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview"); } }); + +erpnext.tally_migration.getError = (traceback) => { + /* Extracts the Error Message from the Python Traceback or Solved error */ + let is_multiline = traceback.trim().indexOf("\n") != -1; + let message; + + if (is_multiline) { + let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1 + let error_line = traceback.substr(exc_error_idx) + let split_str_idx = (error_line.indexOf(':') > 0) ? error_line.indexOf(':') + 1 : 0; + message = error_line.slice(split_str_idx).trim(); + } else { + message = traceback; + } + + return message +} + +erpnext.tally_migration.cleanDoc = (obj) => { + /* Strips all null and empty values of your JSON object */ + let temp = obj; + $.each(temp, function(key, value){ + if (value === "" || value === null){ + delete obj[key]; + } else if (Object.prototype.toString.call(value) === '[object Object]') { + erpnext.tally_migration.cleanDoc(value); + } else if ($.isArray(value)) { + $.each(value, function (k,v) { erpnext.tally_migration.cleanDoc(v); }); + } + }); + return temp; +} + +erpnext.tally_migration.unresolve = (document) => { + /* Mark document migration as unresolved ie. move to failed error log */ + let frm = cur_frm; + let failed_log = erpnext.tally_migration.failed_import_log; + let fixed_log = erpnext.tally_migration.fixed_errors_log; + + let modified_fixed_log = fixed_log.filter(row => { + if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) { + return row + } + }); + + failed_log.push({ doc: document, exc: `Marked unresolved on ${Date()}` }); + + frm.doc.failed_import_log = JSON.stringify(failed_log); + frm.doc.fixed_errors_log = JSON.stringify(modified_fixed_log); + + frm.dirty(); + frm.save(); +} + +erpnext.tally_migration.resolve = (document) => { + /* Mark document migration as resolved ie. move to fixed error log */ + let frm = cur_frm; + let failed_log = erpnext.tally_migration.failed_import_log; + let fixed_log = erpnext.tally_migration.fixed_errors_log; + + let modified_failed_log = failed_log.filter(row => { + if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) { + return row + } + }); + fixed_log.push({ doc: document, exc: `Solved on ${Date()}` }); + + frm.doc.failed_import_log = JSON.stringify(modified_failed_log); + frm.doc.fixed_errors_log = JSON.stringify(fixed_log); + + frm.dirty(); + frm.save(); +} + +erpnext.tally_migration.create_new_doc = (document) => { + /* Mark as resolved and create new document */ + erpnext.tally_migration.resolve(document); + return frappe.call({ + type: "POST", + method: 'erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc', + args: { + document + }, + freeze: true, + callback: function(r) { + if(!r.exc) { + frappe.model.sync(r.message); + frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true; + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); +} + +erpnext.tally_migration.get_html_rows = (logs, field) => { + let index = 0; + let rows = logs + .map(({ doc, exc }) => { + let id = frappe.dom.get_unique_id(); + let traceback = exc; + + let error_message = erpnext.tally_migration.getError(traceback); + index++; + + let show_traceback = ` + +
+
+
${traceback}
+
+
`; + + let show_doc = ` + +
+
+
${JSON.stringify(erpnext.tally_migration.cleanDoc(doc), null, 1)}
+
+
`; + + let create_button = ` + ` + + let mark_as_unresolved = ` + ` + + if (field === "fixed_error_log_preview") { + return ` + ${index} + +
${doc.doctype}
+ + +
${error_message}
+
${show_doc}
+ + +
${mark_as_unresolved}
+ + `; + } else { + return ` + ${index} + +
${doc.doctype}
+ + +
${error_message}
+
${show_traceback}
+
${show_doc}
+ + +
${create_button}
+ + `; + } + }).join(""); + + return rows +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json index dc6f093ac9d..417d9437926 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json @@ -28,14 +28,19 @@ "vouchers", "accounts_section", "default_warehouse", - "round_off_account", + "default_round_off_account", "column_break_21", "default_cost_center", "day_book_section", "day_book_data", "column_break_27", "is_day_book_data_processed", - "is_day_book_data_imported" + "is_day_book_data_imported", + "import_log_section", + "failed_import_log", + "fixed_errors_log", + "failed_import_preview", + "fixed_error_log_preview" ], "fields": [ { @@ -57,6 +62,7 @@ "fieldname": "tally_creditors_account", "fieldtype": "Data", "label": "Tally Creditors Account", + "read_only_depends_on": "eval:doc.is_master_data_processed==1", "reqd": 1 }, { @@ -69,6 +75,7 @@ "fieldname": "tally_debtors_account", "fieldtype": "Data", "label": "Tally Debtors Account", + "read_only_depends_on": "eval:doc.is_master_data_processed==1", "reqd": 1 }, { @@ -92,7 +99,7 @@ "fieldname": "erpnext_company", "fieldtype": "Data", "label": "ERPNext Company", - "read_only_depends_on": "eval:doc.is_master_data_processed == 1" + "read_only_depends_on": "eval:doc.is_master_data_processed==1" }, { "fieldname": "processed_files_section", @@ -136,6 +143,7 @@ }, { "depends_on": "is_master_data_imported", + "description": "The accounts are set by the system automatically but do confirm these defaults", "fieldname": "accounts_section", "fieldtype": "Section Break", "label": "Accounts" @@ -146,12 +154,6 @@ "label": "Default Warehouse", "options": "Warehouse" }, - { - "fieldname": "round_off_account", - "fieldtype": "Link", - "label": "Round Off Account", - "options": "Account" - }, { "fieldname": "column_break_21", "fieldtype": "Column Break" @@ -212,11 +214,47 @@ "fieldname": "default_uom", "fieldtype": "Link", "label": "Default UOM", - "options": "UOM" + "options": "UOM", + "read_only_depends_on": "eval:doc.is_master_data_imported==1" + }, + { + "default": "[]", + "fieldname": "failed_import_log", + "fieldtype": "Code", + "hidden": 1, + "options": "JSON" + }, + { + "fieldname": "failed_import_preview", + "fieldtype": "HTML", + "label": "Failed Import Log" + }, + { + "fieldname": "import_log_section", + "fieldtype": "Section Break", + "label": "Import Log" + }, + { + "fieldname": "default_round_off_account", + "fieldtype": "Link", + "label": "Default Round Off Account", + "options": "Account" + }, + { + "default": "[]", + "fieldname": "fixed_errors_log", + "fieldtype": "Code", + "hidden": 1, + "options": "JSON" + }, + { + "fieldname": "fixed_error_log_preview", + "fieldtype": "HTML", + "label": "Fixed Error Log" } ], "links": [], - "modified": "2020-04-16 13:03:28.894919", + "modified": "2020-04-28 00:29:18.039826", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Tally Migration", diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 13474e19ee1..462685f5e71 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import json import re +import sys import traceback import zipfile from decimal import Decimal @@ -15,18 +16,34 @@ from bs4 import BeautifulSoup as bs import frappe from erpnext import encode_company_abbr from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts +from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import unset_existing_data + from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.model.document import Document from frappe.model.naming import getseries, revert_series_if_last from frappe.utils.data import format_datetime - PRIMARY_ACCOUNT = "Primary" VOUCHER_CHUNK_SIZE = 500 +@frappe.whitelist() +def new_doc(document): + document = json.loads(document) + doctype = document.pop("doctype") + document.pop("name", None) + doc = frappe.new_doc(doctype) + doc.update(document) + + return doc + class TallyMigration(Document): + def validate(self): + failed_import_log = json.loads(self.failed_import_log) + sorted_failed_import_log = sorted(failed_import_log, key=lambda row: row["doc"]["creation"]) + self.failed_import_log = json.dumps(sorted_failed_import_log) + def autoname(self): if not self.name: self.name = "Tally Migration on " + format_datetime(self.creation) @@ -65,9 +82,17 @@ class TallyMigration(Document): "attached_to_name": self.name, "content": json.dumps(value), "is_private": True - }).insert() + }) + try: + f.insert() + except frappe.DuplicateEntryError: + pass setattr(self, key, f.file_url) + def set_account_defaults(self): + self.default_cost_center, self.default_round_off_account = frappe.db.get_value("Company", self.erpnext_company, ["cost_center", "round_off_account"]) + self.default_warehouse = frappe.db.get_value("Stock Settings", "Stock Settings", "default_warehouse") + def _process_master_data(self): def get_company_name(collection): return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip() @@ -84,7 +109,11 @@ class TallyMigration(Document): children, parents = get_children_and_parent_dict(accounts) group_set = [acc[1] for acc in accounts if acc[2]] children, customers, suppliers = remove_parties(parents, children, group_set) - coa = traverse({}, children, roots, roots, group_set) + + try: + coa = traverse({}, children, roots, roots, group_set) + except RecursionError: + self.log(_("Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name")) for account in coa: coa[account]["root_type"] = root_type_map[account] @@ -126,14 +155,18 @@ class TallyMigration(Document): def remove_parties(parents, children, group_set): customers, suppliers = set(), set() for account in parents: + found = False if self.tally_creditors_account in parents[account]: - children.pop(account, None) + found = True if account not in group_set: suppliers.add(account) - elif self.tally_debtors_account in parents[account]: - children.pop(account, None) + if self.tally_debtors_account in parents[account]: + found = True if account not in group_set: customers.add(account) + if found: + children.pop(account, None) + return children, customers, suppliers def traverse(tree, children, accounts, roots, group_set): @@ -151,6 +184,7 @@ class TallyMigration(Document): parties, addresses = [], [] for account in collection.find_all("LEDGER"): party_type = None + links = [] if account.NAME.string.strip() in customers: party_type = "Customer" parties.append({ @@ -161,7 +195,9 @@ class TallyMigration(Document): "territory": "All Territories", "customer_type": "Individual", }) - elif account.NAME.string.strip() in suppliers: + links.append({"link_doctype": party_type, "link_name": account["NAME"]}) + + if account.NAME.string.strip() in suppliers: party_type = "Supplier" parties.append({ "doctype": party_type, @@ -170,6 +206,8 @@ class TallyMigration(Document): "supplier_group": "All Supplier Groups", "supplier_type": "Individual", }) + links.append({"link_doctype": party_type, "link_name": account["NAME"]}) + if party_type: address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")]) addresses.append({ @@ -183,7 +221,7 @@ class TallyMigration(Document): "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, "gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None, - "links": [{"link_doctype": party_type, "link_name": account["NAME"]}], + "links": links }) return parties, addresses @@ -242,12 +280,18 @@ class TallyMigration(Document): def create_company_and_coa(coa_file_url): coa_file = frappe.get_doc("File", {"file_url": coa_file_url}) frappe.local.flags.ignore_chart_of_accounts = True - company = frappe.get_doc({ - "doctype": "Company", - "company_name": self.erpnext_company, - "default_currency": "INR", - "enable_perpetual_inventory": 0, - }).insert() + + try: + company = frappe.get_doc({ + "doctype": "Company", + "company_name": self.erpnext_company, + "default_currency": "INR", + "enable_perpetual_inventory": 0, + }).insert() + except frappe.DuplicateEntryError: + company = frappe.get_doc("Company", self.erpnext_company) + unset_existing_data(self.erpnext_company) + frappe.local.flags.ignore_chart_of_accounts = False create_charts(company.name, custom_chart=json.loads(coa_file.get_content())) company.create_default_warehouses() @@ -256,36 +300,35 @@ class TallyMigration(Document): parties_file = frappe.get_doc("File", {"file_url": parties_file_url}) for party in json.loads(parties_file.get_content()): try: - frappe.get_doc(party).insert() + party_doc = frappe.get_doc(party) + party_doc.insert() except: - self.log(party) + self.log(party_doc) addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url}) for address in json.loads(addresses_file.get_content()): try: - frappe.get_doc(address).insert(ignore_mandatory=True) + address_doc = frappe.get_doc(address) + address_doc.insert(ignore_mandatory=True) except: - try: - gstin = address.pop("gstin", None) - frappe.get_doc(address).insert(ignore_mandatory=True) - self.log({"address": address, "message": "Invalid GSTIN: {}. Address was created without GSTIN".format(gstin)}) - except: - self.log(address) + self.log(address_doc) def create_items_uoms(items_file_url, uoms_file_url): uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url}) for uom in json.loads(uoms_file.get_content()): if not frappe.db.exists(uom): try: - frappe.get_doc(uom).insert() + uom_doc = frappe.get_doc(uom) + uom_doc.insert() except: - self.log(uom) + self.log(uom_doc) items_file = frappe.get_doc("File", {"file_url": items_file_url}) for item in json.loads(items_file.get_content()): try: - frappe.get_doc(item).insert() + item_doc = frappe.get_doc(item) + item_doc.insert() except: - self.log(item) + self.log(item_doc) try: self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4) @@ -299,10 +342,13 @@ class TallyMigration(Document): self.publish("Import Master Data", _("Done"), 4, 4) + self.set_account_defaults() self.is_master_data_imported = 1 + frappe.db.commit() except: self.publish("Import Master Data", _("Process Failed"), -1, 5) + frappe.db.rollback() self.log() finally: @@ -323,7 +369,9 @@ class TallyMigration(Document): processed_voucher = function(voucher) if processed_voucher: vouchers.append(processed_voucher) + frappe.db.commit() except: + frappe.db.rollback() self.log(voucher) return vouchers @@ -349,6 +397,7 @@ class TallyMigration(Document): journal_entry = { "doctype": "Journal Entry", "tally_guid": voucher.GUID.string.strip(), + "tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "", "posting_date": voucher.DATE.string.strip(), "company": self.erpnext_company, "accounts": accounts, @@ -377,6 +426,7 @@ class TallyMigration(Document): "doctype": doctype, party_field: voucher.PARTYNAME.string.strip(), "tally_guid": voucher.GUID.string.strip(), + "tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "", "posting_date": voucher.DATE.string.strip(), "due_date": voucher.DATE.string.strip(), "items": get_voucher_items(voucher, doctype), @@ -468,14 +518,21 @@ class TallyMigration(Document): oldest_year = new_year def create_custom_fields(doctypes): - for doctype in doctypes: - df = { - "fieldtype": "Data", - "fieldname": "tally_guid", - "read_only": 1, - "label": "Tally GUID" - } - create_custom_field(doctype, df) + tally_guid_df = { + "fieldtype": "Data", + "fieldname": "tally_guid", + "read_only": 1, + "label": "Tally GUID" + } + tally_voucher_no_df = { + "fieldtype": "Data", + "fieldname": "tally_voucher_no", + "read_only": 1, + "label": "Tally Voucher Number" + } + for df in [tally_guid_df, tally_voucher_no_df]: + for doctype in doctypes: + create_custom_field(doctype, df) def create_price_list(): frappe.get_doc({ @@ -490,7 +547,7 @@ class TallyMigration(Document): try: frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable") frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable") - frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account) + frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.default_round_off_account) vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) vouchers = json.loads(vouchers_file.get_content()) @@ -521,11 +578,14 @@ class TallyMigration(Document): for index, voucher in enumerate(chunk, start=start): try: - doc = frappe.get_doc(voucher).insert() - doc.submit() + voucher_doc = frappe.get_doc(voucher) + voucher_doc.insert() + voucher_doc.submit() self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total) + frappe.db.commit() except: - self.log(voucher) + frappe.db.rollback() + self.log(voucher_doc) if is_last: self.status = "" @@ -551,9 +611,22 @@ class TallyMigration(Document): frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600) def log(self, data=None): - data = data or self.status - message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) - return frappe.log_error(title="Tally Migration Error", message=message) + if isinstance(data, frappe.model.document.Document): + if sys.exc_info()[1].__class__ != frappe.DuplicateEntryError: + failed_import_log = json.loads(self.failed_import_log) + doc = data.as_dict() + failed_import_log.append({ + "doc": doc, + "exc": traceback.format_exc() + }) + self.failed_import_log = json.dumps(failed_import_log, separators=(',', ':')) + self.save() + frappe.db.commit() + + else: + data = data or self.status + message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) + return frappe.log_error(title="Tally Migration Error", message=message) def set_status(self, status=""): self.status = status diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py index 0c13b6af9db..3dc7c1ec393 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py @@ -70,6 +70,7 @@ def validate_service_item(item, msg): if frappe.db.get_value('Item', item, 'is_stock_item'): frappe.throw(_(msg)) +@frappe.whitelist() def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None): fields = ['name', 'practitioner_name', 'mobile_phone'] diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 4c2d3f692a2..2bef5fb5bdd 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -15,6 +15,7 @@ class TestInpatientRecord(unittest.TestCase): patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 ip_record.save(ignore_permissions = True) self.assertEqual(ip_record.name, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(ip_record.status, frappe.db.get_value("Patient", patient, "inpatient_status")) @@ -26,7 +27,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) # Discharge - schedule_discharge(patient=patient) + schedule_discharge(frappe.as_json({'patient': patient})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) @@ -44,8 +45,10 @@ class TestInpatientRecord(unittest.TestCase): patient = create_patient() ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 ip_record.save(ignore_permissions = True) ip_record_new = create_inpatient(patient) + ip_record_new.expected_length_of_stay = 0 self.assertRaises(frappe.ValidationError, ip_record_new.save) service_unit = get_healthcare_service_unit() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ab161aa9f51..2a695896ed0 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -41,7 +41,7 @@ boot_session = "erpnext.startup.boot.boot_session" notification_config = "erpnext.startup.notifications.get_notification_config" get_help_messages = "erpnext.utilities.activation.get_help_messages" leaderboards = "erpnext.startup.leaderboard.get_leaderboards" - +filters_config = "erpnext.startup.filters.get_filters_config" on_session_creation = [ "erpnext.portal.utils.create_customer_or_supplier", @@ -238,6 +238,9 @@ doc_events = { "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_trash": "erpnext.regional.check_deletion_permission" }, + "Purchase Invoice": { + "on_submit": "erpnext.regional.india.utils.make_reverse_charge_entries" + }, "Payment Entry": { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status"], "on_trash": "erpnext.regional.check_deletion_permission" @@ -320,8 +323,7 @@ scheduler_events = { "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans" ], "monthly_long": [ - "erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", - "erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense", + "erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" ] diff --git a/erpnext/hr/desk_page/hr/hr.json b/erpnext/hr/desk_page/hr/hr.json index 7ac000b011a..12548d48a74 100644 --- a/erpnext/hr/desk_page/hr/hr.json +++ b/erpnext/hr/desk_page/hr/hr.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Leaves", - "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -93,7 +93,7 @@ "idx": 0, "is_standard": 1, "label": "HR", - "modified": "2020-05-28 13:36:07.710600", + "modified": "2020-06-16 19:20:50.976045", "modified_by": "Administrator", "module": "HR", "name": "HR", @@ -126,7 +126,7 @@ }, { "label": "Salary Structure", - "link_to": "Payroll Entry", + "link_to": "Salary Structure", "type": "DocType" }, { diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index 906f6f77f21..a656a7ea5f7 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -19,11 +19,15 @@ "attendance_date", "company", "department", - "shift", "attendance_request", - "amended_from", + "details_section", + "shift", + "in_time", + "out_time", + "column_break_18", "late_entry", - "early_exit" + "early_exit", + "amended_from" ], "fields": [ { @@ -172,13 +176,36 @@ "fieldname": "early_exit", "fieldtype": "Check", "label": "Early Exit" + }, + { + "fieldname": "details_section", + "fieldtype": "Section Break", + "label": "Details" + }, + { + "depends_on": "shift", + "fieldname": "in_time", + "fieldtype": "Datetime", + "label": "In Time", + "read_only": 1 + }, + { + "depends_on": "shift", + "fieldname": "out_time", + "fieldtype": "Datetime", + "label": "Out Time", + "read_only": 1 + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" } ], "icon": "fa fa-ok", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-11 11:40:14.319496", + "modified": "2020-05-29 13:51:37.177231", "modified_by": "Administrator", "module": "HR", "name": "Attendance", diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index df0f75a18c3..d4c118f802d 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -19,7 +19,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) + employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True) employee_department = filters.get("department") or employee.department if employee_department: @@ -33,10 +33,16 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): if filters.get("doctype") == "Leave Application" and employee.leave_approver: approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name'])) + if filters.get("doctype") == "Expense Claim" and employee.expense_approver: + approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name'])) + + if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" + field_name = "Leave Approver" else: parentfield = "expense_approvers" + field_name = "Expense Approver" if department_list: for d in department_list: approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from @@ -46,4 +52,12 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): and approver.parentfield = %s and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) + if len(approvers) == 0: + frappe.throw(_("Please set {0} for the Employee or for Department: {1}"). + format( + field_name, frappe.bold(employee_department), + frappe.bold(employee.name) + ), + title=_(field_name + " Missing")) + return set(tuple(approver) for approver in approvers) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index f575765f69b..7dacacf12b0 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -62,6 +62,7 @@ "salary_mode", "payroll_cost_center", "column_break_52", + "expense_approver", "bank_name", "bank_ac_no", "health_insurance_section", @@ -205,7 +206,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nActive\nLeft", + "options": "Active\nLeft", "reqd": 1, "search_index": 1 }, @@ -667,6 +668,7 @@ "oldfieldtype": "Date" }, { + "depends_on": "eval:doc.status == \"Left\"", "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", @@ -797,13 +799,21 @@ { "fieldname": "column_break_52", "fieldtype": "Column Break" + }, + { + "fieldname": "expense_approver", + "fieldtype": "Link", + "label": "Expense Approver", + "options": "User", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2020-05-05 18:51:03.152503", + "modified": "2020-06-18 18:01:27.223535", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 6cc49cfff23..cba8ee9a404 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -139,13 +139,13 @@ frappe.ui.form.on('Employee Advance', { employee: function (frm) { if (frm.doc.employee) { return frappe.call({ - method: "erpnext.hr.doctype.employee_advance.employee_advance.get_due_advance_amount", + method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount", args: { "employee": frm.doc.employee, "posting_date": frm.doc.posting_date }, callback: function(r) { - frm.set_value("due_advance_amount",r.message); + frm.set_value("pending_amount",r.message); } }); } diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 8c5ce42d870..0d909138719 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -19,7 +19,7 @@ "column_break_11", "advance_amount", "paid_amount", - "due_advance_amount", + "pending_amount", "claimed_amount", "return_amount", "section_break_7", @@ -102,14 +102,6 @@ "options": "Company:company:default_currency", "read_only": 1 }, - { - "depends_on": "eval:cur_frm.doc.employee", - "fieldname": "due_advance_amount", - "fieldtype": "Currency", - "label": "Due Advance Amount", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "fieldname": "claimed_amount", "fieldtype": "Currency", @@ -177,11 +169,19 @@ "fieldname": "repay_unclaimed_amount_from_salary", "fieldtype": "Check", "label": "Repay unclaimed amount from salary" + }, + { + "depends_on": "eval:cur_frm.doc.employee", + "fieldname": "pending_amount", + "fieldtype": "Currency", + "label": "Pending Amount", + "options": "Company:company:default_currency", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-03-06 15:11:33.747535", + "modified": "2020-06-12 12:42:39.833818", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index c5a9fcec1d4..b290bcc4086 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -95,7 +95,7 @@ class EmployeeAdvance(Document): frappe.db.set_value("Employee Advance", self.name, "status", self.status) @frappe.whitelist() -def get_due_advance_amount(employee, posting_date): +def get_pending_amount(employee, posting_date): employee_due_amount = frappe.get_all("Employee Advance", \ filters = {"employee":employee, "docstatus":1, "posting_date":("<=", posting_date)}, \ fields = ["advance_amount", "paid_amount"]) diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 86705121ac5..15fbd4e0153 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -72,7 +72,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N return doc -def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, shift=None): +def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None): """Creates an attendance and links the attendance to the Employee Checkin. Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown. @@ -100,7 +100,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki 'company': employee_doc.company, 'shift': shift, 'late_entry': late_entry, - 'early_exit': early_exit + 'early_exit': early_exit, + 'in_time': in_time, + 'out_time': out_time } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index fb2310396b7..6bb9af98263 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -243,7 +243,6 @@ frappe.ui.form.on("Expense Claim", { }, update_employee_advance_claimed_amount: function(frm) { - console.log("update_employee_advance_claimed_amount") let amount_to_be_allocated = frm.doc.grand_total; $.each(frm.doc.advances || [], function(i, advance){ if (amount_to_be_allocated >= advance.unclaimed_amount){ @@ -295,6 +294,16 @@ frappe.ui.form.on("Expense Claim", { frm.events.get_advances(frm); }, + cost_center: function(frm) { + frm.events.set_child_cost_center(frm); + }, + set_child_cost_center: function(frm){ + (frm.doc.expenses || []).forEach(function(d) { + if (!d.cost_center){ + d.cost_center = frm.doc.cost_center; + } + }); + }, get_taxes: function(frm) { if(frm.doc.taxes) { frappe.call({ @@ -338,8 +347,7 @@ frappe.ui.form.on("Expense Claim", { frappe.ui.form.on("Expense Claim Detail", { expenses_add: function(frm, cdt, cdn) { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("expenses", row, ["cost_center"]); + frm.events.set_child_cost_center(frm); }, amount: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 96baaab5950..fa28470af89 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -66,6 +66,7 @@ "fieldname": "employee", "fieldtype": "Link", "in_global_search": 1, + "in_standard_filter": 1, "label": "From Employee", "oldfieldname": "employee", "oldfieldtype": "Link", @@ -164,6 +165,7 @@ "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", + "in_standard_filter": 1, "label": "Posting Date", "oldfieldname": "posting_date", "oldfieldtype": "Date", @@ -236,6 +238,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Company", "oldfieldname": "company", "oldfieldtype": "Link", @@ -368,7 +371,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2019-12-14 23:52:05.388458", + "modified": "2020-06-15 12:43:04.099803", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 1f50e27098c..fb1f2c00b13 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -38,11 +38,15 @@ frappe.ui.form.on("Leave Application", { }, validate: function(frm) { + if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){ + frm.doc.half_day_date = frm.doc.from_date; + } frm.toggle_reqd("half_day_date", frm.doc.half_day == 1); }, make_dashboard: function(frm) { var leave_details; + let lwps; if (frm.doc.employee) { frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", @@ -58,6 +62,7 @@ frappe.ui.form.on("Leave Application", { if (!r.exc && r.message['leave_approver']) { frm.set_value('leave_approver', r.message['leave_approver']); } + lwps = r.message["lwps"]; } }); $("div").remove(".form-dashboard-section.custom"); @@ -67,6 +72,18 @@ frappe.ui.form.on("Leave Application", { }) ); frm.dashboard.show(); + let allowed_leave_types = Object.keys(leave_details); + + // lwps should be allowed, lwps don't have any allocation + allowed_leave_types = allowed_leave_types.concat(lwps); + + frm.set_query('leave_type', function(){ + return { + filters : [ + ['leave_type_name', 'in', allowed_leave_types] + ] + }; + }); } }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index f2968bcd889..0423824c0e9 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -19,7 +19,6 @@ class NotAnOptionalHoliday(frappe.ValidationError): pass from frappe.model.document import Document class LeaveApplication(Document): - def get_feed(self): return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) @@ -33,6 +32,7 @@ class LeaveApplication(Document): self.validate_block_days() self.validate_salary_processed_days() self.validate_attendance() + self.set_half_day_date() if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'): self.validate_optional_leave() self.validate_applicable_after() @@ -131,8 +131,6 @@ class LeaveApplication(Document): for dt in daterange(getdate(self.from_date), getdate(self.to_date)): date = dt.strftime("%Y-%m-%d") status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave" - print("-------->>>", status) - # frappe.throw("Hello") attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee, attendance_date = date, docstatus = ('!=', 2))) @@ -292,6 +290,10 @@ class LeaveApplication(Document): frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday) day = add_days(day, 1) + def set_half_day_date(self): + if self.from_date == self.to_date and self.half_day == 1: + self.half_day_date = self.from_date + def notify_employee(self): employee = frappe.get_doc("Employee", self.employee) if not employee.user_id: @@ -442,6 +444,7 @@ def get_leave_details(employee, date): total_allocated_leaves = frappe.db.get_value('Leave Allocation', { 'from_date': ('<=', date), 'to_date': ('>=', date), + 'employee': employee, 'leave_type': allocation.leave_type, }, 'SUM(total_leaves_allocated)') or 0 @@ -459,9 +462,14 @@ def get_leave_details(employee, date): "pending_leaves": leaves_pending, "remaining_leaves": remaining_leaves} + #is used in set query + lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) + lwps = [lwp.name for lwp in lwps] + ret = { 'leave_allocation': leave_allocation, - 'leave_approver': get_leave_approver(employee) + 'leave_approver': get_leave_approver(employee), + 'lwps': lwps } return ret diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 295f3b43419..d30e3b9f9c6 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -1,5 +1,5 @@ -{% if data %} +{% if not jQuery.isEmptyObject(data) %}
{{ __("Allocated Leaves") }}
@@ -11,7 +11,6 @@ - {% for(const [key, value] of Object.entries(data)) { %} @@ -26,6 +25,6 @@ {% } %}
{{ __("Pending Leaves") }} {{ __("Available Leaves") }}
-{% } else { %} +{% else %}

No Leaves have been allocated.

-{% } %} \ No newline at end of file +{% endif %} \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index d56080eecd4..19735648aa9 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -28,13 +28,14 @@ class ShiftType(Document): logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])): single_shift_logs = list(group) - attendance_status, working_hours, late_entry, early_exit = self.get_attendance(single_shift_logs) - mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, self.name) + attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs) + mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name) for employee in self.get_assigned_employee(self.process_attendance_after, True): self.mark_absent_for_dates_with_no_attendance(employee) def get_attendance(self, logs): - """Return attendance_status, working_hours for a set of logs belonging to a single shift. + """Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time + for a set of logs belonging to a single shift. Assumtion: 1. These logs belongs to an single shift, single employee and is not in a holiday date. 2. Logs are in chronological order @@ -48,10 +49,10 @@ class ShiftType(Document): early_exit = True if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: - return 'Absent', total_working_hours, late_entry, early_exit + return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: - return 'Half Day', total_working_hours, late_entry, early_exit - return 'Present', total_working_hours, late_entry, early_exit + return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time + return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time def mark_absent_for_dates_with_no_attendance(self, employee): """Marks Absents for the given employee on working days in this shift which have no attendance marked. diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index cd125108c61..8d95924681a 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -73,11 +73,11 @@ class EmployeeBoardingController(Document): def assign_task_to_users(self, task, users): for user in users: args = { - 'assign_to' : user, - 'doctype' : task.doctype, - 'name' : task.name, - 'description' : task.description or task.subject, - 'notify': self.notify_users_by_email + 'assign_to': [user], + 'doctype': task.doctype, + 'name': task.name, + 'description': task.description or task.subject, + 'notify': self.notify_users_by_email } assign_to.add(args) diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index d79860a3520..48193b0a0d8 100644 --- a/erpnext/loan_management/desk_page/loan/loan.json +++ b/erpnext/loan_management/desk_page/loan/loan.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Loan Repayment\"\n ],\n \"doctype\": \"Loan Repayment\",\n \"incomplete_dependencies\": [\n \"Loan Repayment\"\n ],\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Loan Security Pledge\"\n ],\n \"doctype\": \"Loan Security Pledge\",\n \"incomplete_dependencies\": [\n \"Loan Security Pledge\"\n ],\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -34,11 +34,10 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Loan", - "modified": "2020-05-28 13:37:42.017709", + "modified": "2020-06-07 19:42:14.947902", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index c9e36a84ddb..d44088bee74 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -27,6 +27,7 @@ class LoanDisbursement(AccountsController): def on_cancel(self): self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] def set_missing_values(self): if not self.disbursement_date: diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 094b9c698c7..e6ceb551850 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -31,6 +31,7 @@ class LoanInterestAccrual(AccountsController): self.update_is_accrued() self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] def update_is_accrued(self): frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0) @@ -176,21 +177,23 @@ def get_term_loans(date, term_loan=None, loan_type=None): return term_loans def make_loan_interest_accrual_entry(args): - loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") - loan_interest_accrual.loan = args.loan - loan_interest_accrual.applicant_type = args.applicant_type - loan_interest_accrual.applicant = args.applicant - loan_interest_accrual.interest_income_account = args.interest_income_account - loan_interest_accrual.loan_account = args.loan_account - loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, 2) - loan_interest_accrual.interest_amount = flt(args.interest_amount, 2) - loan_interest_accrual.posting_date = args.posting_date or nowdate() - loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest - loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name - loan_interest_accrual.payable_principal_amount = args.payable_principal + precision = cint(frappe.db.get_default("currency_precision")) or 2 - loan_interest_accrual.save() - loan_interest_accrual.submit() + loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") + loan_interest_accrual.loan = args.loan + loan_interest_accrual.applicant_type = args.applicant_type + loan_interest_accrual.applicant = args.applicant + loan_interest_accrual.interest_income_account = args.interest_income_account + loan_interest_accrual.loan_account = args.loan_account + loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) + loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) + loan_interest_accrual.posting_date = args.posting_date or nowdate() + loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest + loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name + loan_interest_accrual.payable_principal_amount = args.payable_principal + + loan_interest_accrual.save() + loan_interest_accrual.submit() def get_no_of_days_for_interest_accural(loan, posting_date): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 2ab668a0e11..c28994e280b 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext import json from frappe import _ -from frappe.utils import flt, getdate +from frappe.utils import flt, getdate, cint from six import iteritems from frappe.model.document import Document from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime @@ -29,8 +29,11 @@ class LoanRepayment(AccountsController): def on_cancel(self): self.mark_as_unpaid() self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] def set_missing_values(self, amounts): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + if not self.posting_date: self.posting_date = get_datetime() @@ -38,24 +41,26 @@ class LoanRepayment(AccountsController): self.cost_center = erpnext.get_default_cost_center(self.company) if not self.interest_payable: - self.interest_payable = flt(amounts['interest_amount'], 2) + self.interest_payable = flt(amounts['interest_amount'], precision) if not self.penalty_amount: - self.penalty_amount = flt(amounts['penalty_amount'], 2) + self.penalty_amount = flt(amounts['penalty_amount'], precision) if not self.pending_principal_amount: - self.pending_principal_amount = flt(amounts['pending_principal_amount'], 2) + self.pending_principal_amount = flt(amounts['pending_principal_amount'], precision) if not self.payable_principal_amount and self.is_term_loan: - self.payable_principal_amount = flt(amounts['payable_principal_amount'], 2) + self.payable_principal_amount = flt(amounts['payable_principal_amount'], precision) if not self.payable_amount: - self.payable_amount = flt(amounts['payable_amount'], 2) + self.payable_amount = flt(amounts['payable_amount'], precision) if amounts.get('due_date'): self.due_date = amounts.get('due_date') def validate_amount(self): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + if not self.amount_paid: frappe.throw(_("Amount paid cannot be zero")) @@ -63,11 +68,13 @@ class LoanRepayment(AccountsController): msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) frappe.throw(msg) - if self.payment_type == "Loan Closure" and flt(self.amount_paid, 2) < flt(self.payable_amount, 2): + if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision): msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) frappe.throw(msg) def update_paid_amount(self): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + loan = frappe.get_doc("Loan", self.against_loan) for payment in self.repayment_details: @@ -75,9 +82,9 @@ class LoanRepayment(AccountsController): SET paid_principal_amount = `paid_principal_amount` + %s, paid_interest_amount = `paid_interest_amount` + %s WHERE name = %s""", - (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) + (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual)) - if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): + if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision): if loan.is_secured_loan: frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") else: @@ -253,6 +260,7 @@ def get_accrued_interest_entries(against_loan): # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable def get_amounts(amounts, against_loan, posting_date, payment_type): + precision = cint(frappe.db.get_default("currency_precision")) or 2 against_loan_doc = frappe.get_doc("Loan", against_loan) loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) @@ -282,8 +290,8 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): payable_principal_amount += entry.payable_principal_amount pending_accrual_entries.setdefault(entry.name, { - 'interest_amount': flt(entry.interest_amount), - 'payable_principal_amount': flt(entry.payable_principal_amount) + 'interest_amount': flt(entry.interest_amount, precision), + 'payable_principal_amount': flt(entry.payable_principal_amount, precision) }) if not final_due_date: @@ -301,11 +309,11 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 total_pending_interest += (pending_days * per_day_interest) - amounts["pending_principal_amount"] = pending_principal_amount - amounts["payable_principal_amount"] = payable_principal_amount - amounts["interest_amount"] = total_pending_interest - amounts["penalty_amount"] = penalty_amount - amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount + amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) + amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) + amounts["interest_amount"] = flt(total_pending_interest, precision) + amounts["penalty_amount"] = flt(penalty_amount, precision) + amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["pending_accrual_entries"] = pending_accrual_entries if final_due_date: diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 51c5cb98a63..1dd3710cd2a 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -41,6 +41,7 @@ "options": "Company:company:default_currency" }, { + "default": "0", "fieldname": "rate_of_interest", "fieldtype": "Percent", "label": "Rate of Interest (%) Yearly", @@ -143,7 +144,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-15 00:24:43.259963", + "modified": "2020-06-07 18:55:59.346292", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py index ea6a2ee6452..19518554759 100644 --- a/erpnext/loan_management/report/loan_security_status/loan_security_status.py +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py @@ -76,7 +76,8 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "currency", "options": "Currency", - "width": 50 + "width": 50, + "hidden": 1 } ] @@ -84,17 +85,13 @@ def get_columns(filters): def get_data(filters): - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security", - fields=["name", "loan_security_price"], as_list=1 - )) - data = [] conditions = get_conditions(filters) loan_security_pledges = frappe.db.sql(""" SELECT p.name, p.applicant, p.loan, p.status, p.pledge_time, - c.loan_security, c.qty + c.loan_security, c.qty, c.loan_security_price, c.amount FROM `tabLoan Security Pledge` p, `tabPledge` c WHERE @@ -115,8 +112,8 @@ def get_data(filters): row["pledge_time"] = pledge.pledge_time row["loan_security"] = pledge.loan_security row["qty"] = pledge.qty - row["loan_security_price"] = loan_security_price_map.get(pledge.loan_security) - row["loan_security_value"] = row["loan_security_price"] * pledge.qty + row["loan_security_price"] = pledge.loan_security_price + row["loan_security_value"] = pledge.amount row["currency"] = default_currency data.append(row) diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py index 4a17fd07fbb..64e4bc6ed06 100644 --- a/erpnext/manufacturing/dashboard_fixtures.py +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -4,7 +4,6 @@ import frappe, erpnext, json from frappe import _ from frappe.utils import nowdate, get_first_day, get_last_day, add_months -from erpnext.accounts.utils import get_fiscal_year def get_data(): return frappe._dict({ diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 1bdac5731ef..7d31a1cd15e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -112,8 +112,16 @@ class BOM(WebsiteGenerator): if self.routing: self.set("operations", []) for d in frappe.get_all("BOM Operation", fields = ["*"], - filters = {'parenttype': 'Routing', 'parent': self.routing}): - child = self.append('operations', d) + filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="idx"): + child = self.append('operations', { + "operation": d.operation, + "workstation": d.workstation, + "description": d.description, + "time_in_mins": d.time_in_mins, + "batch_size": d.batch_size, + "operating_cost": d.operating_cost, + "idx": d.idx + }) child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2) def set_bom_material_details(self): diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 3ca851d783b..0350e2cb374 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -78,6 +78,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.doctype == 'BOM'", "fieldname": "base_hour_rate", "fieldtype": "Currency", "label": "Base Hour Rate(Company Currency)", @@ -87,6 +88,7 @@ }, { "default": "5", + "depends_on": "eval:parent.doctype == 'BOM'", "fieldname": "base_operating_cost", "fieldtype": "Currency", "label": "Operating Cost(Company Currency)", @@ -108,12 +110,12 @@ ], "idx": 1, "istable": 1, - "modified": "2019-07-16 22:35:55.374037", - "modified_by": "govindsmenokee@gmail.com", + "modified": "2020-06-16 17:01:11.128420", + "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js index 6cfd0bae5b5..d7589fa3907 100644 --- a/erpnext/manufacturing/doctype/routing/routing.js +++ b/erpnext/manufacturing/doctype/routing/routing.js @@ -44,7 +44,6 @@ frappe.ui.form.on('BOM Operation', { name: d.workstation }, callback: function (data) { - frappe.model.set_value(d.doctype, d.name, "base_hour_rate", data.message.hour_rate); frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate); frm.events.calculate_operating_cost(frm, d); } diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index cac80677297..2ca9f1694b3 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -217,6 +217,8 @@ class ForecastingReport(ExponentialSmoothingForecast): } def get_summary_data(self): + if not self.data: return + return [ { "value": sum(self.total_demand), diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 571f87af874..d1294ccc08b 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -77,6 +77,7 @@ def create_customer(user_details): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" + customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) try: @@ -91,7 +92,11 @@ def create_customer(user_details): "link_name": customer.name }) - contact.insert() + contact.save() + + except frappe.DuplicateEntryError: + return customer.name + except Exception as e: frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) pass diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index df19995a1c5..7a0caed621e 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -62,11 +62,26 @@ def get_member_based_on_subscription(subscription_id, email): 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") - return frappe.get_doc("Member", members[0]['name']) + try: + return frappe.get_doc("Member", members[0]['name']) + except: + return None + +def verify_signature(data): + signature = frappe.request.headers.get('X-Razorpay-Signature') + + settings = frappe.get_doc("Membership Settings") + key = settings.get_webhook_secret() + + controller = frappe.get_doc("Razorpay Settings") + + controller.verify_signature(data, signature, key) + @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): - data = frappe.request.get_data() + data = frappe.request.get_data(as_text=True) + verify_signature(data) if isinstance(data, six.string_types): data = json.loads(data) @@ -84,7 +99,10 @@ def trigger_razorpay_subscription(*args, **kwargs): except Exception as e: error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) notify_failure(error_log) - raise e + return False + + if not member: + return False if data.event == "subscription.activated": member.customer_id = payment.customer_id @@ -113,7 +131,6 @@ def trigger_razorpay_subscription(*args, **kwargs): return True - def notify_failure(log): try: content = """Dear System Manager, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index c01a0b23d5d..8c0e3a4fa76 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -1,8 +1,30 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Membership Settings', { +frappe.ui.form.on("Membership Settings", { refresh: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Revoke "), () => { + frm.call("revoke_key").then(() => { + frm.refresh(); + }) + }); + } + frm.trigger("add_generate_button"); + }, - } + add_generate_button: function(frm) { + let label; + + if (frm.doc.webhook_secret) { + label = __("Regenerate Webhook Secret"); + } else { + label = __("Generate Webhook Secret"); + } + frm.add_custom_button(label, () => { + frm.call("generate_webhook_key").then(() => { + frm.refresh(); + }); + }); + }, }); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 56b8eac4b12..52b9d01088b 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -8,7 +8,8 @@ "enable_razorpay", "razorpay_settings_section", "billing_cycle", - "billing_frequency" + "billing_frequency", + "webhook_secret" ], "fields": [ { @@ -34,11 +35,17 @@ "fieldname": "billing_frequency", "fieldtype": "Int", "label": "Billing Frequency" + }, + { + "fieldname": "webhook_secret", + "fieldtype": "Password", + "label": "Webhook Secret", + "read_only": 1 } ], "issingle": 1, "links": [], - "modified": "2020-04-07 18:42:51.496807", + "modified": "2020-05-22 12:38:27.103759", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py index 2b8e37f2a65..f3b2eee6f97 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.py @@ -4,11 +4,27 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.integrations.utils import get_payment_gateway_controller from frappe.model.document import Document class MembershipSettings(Document): - pass + def generate_webhook_key(self): + key = frappe.generate_hash(length=20) + self.webhook_secret = key + self.save() + + frappe.msgprint( + _("Here is your webhook secret, this will be shown to you only once.") + "

" + key, + _("Webhook Secret") + ); + + def revoke_key(self): + self.webhook_secret = None; + self.save() + + def get_webhook_secret(self): + return self.get_password(fieldname="webhook_secret", raise_exception=False) @frappe.whitelist() def get_plans_for_membership(*args, **kwargs): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7125abfb696..e9ef6c08548 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -680,6 +680,8 @@ erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22 erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive +execute:frappe.reload_doc("HR", "doctype", "Employee Advance") +erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount 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 @@ -695,4 +697,7 @@ execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) erpnext.patches.v12_0.update_uom_conversion_factor erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions -erpnext.patches.v12_0.unhide_cost_center_field \ No newline at end of file +erpnext.patches.v12_0.unhide_cost_center_field +erpnext.patches.v13_0.update_sla_enhancements +erpnext.patches.v12_0.update_address_template_for_india +erpnext.patches.v12_0.set_multi_uom_in_rfq diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py index 3c9758eb84a..1ddbae6cd29 100644 --- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py +++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py @@ -4,7 +4,7 @@ import frappe def execute(): frappe.reload_doc('accounts', 'doctype', 'bank', force=1) - if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account'): + if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): frappe.db.sql(""" UPDATE `tabBank` b, `tabBank Account` ba SET b.swift_number = ba.swift_number, b.branch_code = ba.branch_code @@ -12,4 +12,4 @@ def execute(): """) frappe.reload_doc('accounts', 'doctype', 'bank_account') - frappe.reload_doc('accounts', 'doctype', 'payment_request') \ No newline at end of file + frappe.reload_doc('accounts', 'doctype', 'payment_request') diff --git a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py new file mode 100644 index 00000000000..6013eaa29c6 --- /dev/null +++ b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py @@ -0,0 +1,11 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + ''' Move from due_advance_amount to pending_amount ''' + + if frappe.db.has_column("Employee Advance", "due_advance_amount"): + frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''') diff --git a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py new file mode 100644 index 00000000000..70ca6b222e9 --- /dev/null +++ b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py @@ -0,0 +1,16 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt +from erpnext.stock.get_item_details import get_conversion_factor + +def execute(): + frappe.reload_doc('buying', 'doctype', 'request_for_quotation_item') + + frappe.db.sql("""UPDATE `tabRequest for Quotation Item` + SET + stock_uom = uom, + conversion_factor = 1, + stock_qty = qty""") \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_address_template_for_india.py b/erpnext/patches/v12_0/update_address_template_for_india.py new file mode 100644 index 00000000000..0d582da4b5c --- /dev/null +++ b/erpnext/patches/v12_0/update_address_template_for_india.py @@ -0,0 +1,12 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.regional.address_template.setup import set_up_address_templates + +def execute(): + if frappe.db.get_value('Company', {'country': 'India'}, 'name'): + address_template = frappe.db.get_value('Address Template', 'India', 'template') + if not address_template or "gstin" not in address_template: + set_up_address_templates(default_country='India') diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index ec94cd01d1e..5ade8ca0f4c 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -14,15 +14,21 @@ def execute(): frappe.reload_doc("hr", "doctype", doctype) + standard_tax_exemption_amount_exists = frappe.db.has_column("Payroll Period", "standard_tax_exemption_amount") + + select_fields = "name, start_date, end_date" + if standard_tax_exemption_amount_exists: + select_fields = "name, start_date, end_date, standard_tax_exemption_amount" + for company in frappe.get_all("Company"): payroll_periods = frappe.db.sql(""" SELECT - name, start_date, end_date, standard_tax_exemption_amount + {0} FROM `tabPayroll Period` WHERE company=%s ORDER BY start_date DESC - """, company.name, as_dict = 1) + """.format(select_fields), company.name, as_dict = 1) for i, period in enumerate(payroll_periods): income_tax_slab = frappe.new_doc("Income Tax Slab") @@ -36,7 +42,8 @@ def execute(): income_tax_slab.effective_from = period.start_date income_tax_slab.company = company.name income_tax_slab.allow_tax_exemption = 1 - income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount + if standard_tax_exemption_amount_exists: + income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount income_tax_slab.flags.ignore_mandatory = True income_tax_slab.submit() diff --git a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py index a7d4c665a1e..be5e30f3074 100644 --- a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py +++ b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py @@ -7,4 +7,4 @@ def execute(): for entry in doctypes: if frappe.db.exists('DocType', entry): frappe.reload_doc('Healthcare', 'doctype', entry) - frappe.db.sql("update `tab{dt}` set company = '{company}' where ifnull(company, '') = ''".format(dt=entry, company=company)) + frappe.db.sql("update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format(dt=entry, company=frappe.db.escape(company))) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py new file mode 100644 index 00000000000..c156ba95772 --- /dev/null +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -0,0 +1,93 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + # add holiday list and employee group fields in SLA + # change response and resolution time in priorities child table + if frappe.db.exists('DocType', 'Service Level Agreement'): + sla_details = frappe.db.get_all('Service Level Agreement', fields=['name', 'service_level']) + priorities = frappe.db.get_all('Service Level Priority', fields=['*'], filters={ + 'parenttype': ('in', ['Service Level Agreement', 'Service Level']) + }) + + frappe.reload_doc('support', 'doctype', 'service_level_agreement') + frappe.reload_doc('support', 'doctype', 'pause_sla_on_status') + frappe.reload_doc('support', 'doctype', 'service_level_priority') + frappe.reload_doc('support', 'doctype', 'service_day') + + for entry in sla_details: + values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group']) + if values: + holiday_list = values[0] + employee_group = values[1] + frappe.db.set_value('Service Level Agreement', entry.name, { + 'holiday_list': holiday_list, + 'employee_group': employee_group + }) + + priority_dict = {} + + for priority in priorities: + if priority.parenttype == 'Service Level Agreement': + response_time = convert_to_seconds(priority.response_time, priority.response_time_period) + resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period) + frappe.db.set_value('Service Level Priority', priority.name, { + 'response_time': response_time, + 'resolution_time': resolution_time + }) + if priority.parenttype == 'Service Level': + if not priority.parent in priority_dict: + priority_dict[priority.parent] = [] + priority_dict[priority.parent].append(priority) + + + # copy Service Levels to Service Level Agreements + sl = [entry.service_level for entry in sla_details] + if frappe.db.exists('DocType', 'Service Level'): + service_levels = frappe.db.get_all('Service Level', filters={'service_level': ('not in', sl)}, fields=['*']) + for entry in service_levels: + sla = frappe.new_doc('Service Level Agreement') + sla.service_level = entry.service_level + sla.holiday_list = entry.holiday_list + sla.employee_group = entry.employee_group + sla.flags.ignore_validate = True + sla = sla.insert(ignore_mandatory=True) + + frappe.db.sql(""" + UPDATE + `tabService Day` + SET + parent = %(new_parent)s , parentfield = 'support_and_resolution', parenttype = 'Service Level Agreement' + WHERE + parent = %(old_parent)s + """, {'new_parent': sla.name, 'old_parent': entry.name}, as_dict = 1) + + priority_list = priority_dict.get(entry.name) + if priority_list: + sla = frappe.get_doc('Service Level Agreement', sla.name) + for priority in priority_list: + row = sla.append('priorities', { + 'priority': priority.priority, + 'default_priority': priority.default_priority, + 'response_time': convert_to_seconds(priority.response_time, priority.response_time_period), + 'resolution_time': convert_to_seconds(priority.resolution_time, priority.resolution_time_period) + }) + row.db_update() + sla.db_update() + + frappe.delete_doc_if_exists('DocType', 'Service Level') + + +def convert_to_seconds(value, unit): + seconds = 0 + if unit == "Hour": + seconds = value * 3600 + if unit == "Day": + seconds = value * 3600 * 24 + if unit == "Week": + seconds = value * 3600 * 24 * 7 + return seconds diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 0993e69e042..6b6b8c579be 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -1,4 +1,5 @@ import frappe +from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager def get_field_filter_data(): @@ -243,6 +244,8 @@ def get_next_attribute_and_values(item_code, selected_attributes): else: product_info = None + product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) + return { 'next_attribute': next_attribute, 'valid_options_for_attributes': valid_options_for_attributes, diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 58629634968..3570a0f2be4 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -18,7 +18,7 @@ frappe.ui.form.on("Project", { }; }, onload: function (frm) { - var so = frm.get_docfield("Project", "sales_order"); + var so = frappe.meta.get_docfield("Project", "sales_order"); so.get_route_options_for_new_doc = function (field) { if (frm.is_new()) return; return { @@ -135,4 +135,4 @@ function open_form(frm, doctype, child_doctype, parentfield) { frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); -} +} \ No newline at end of file diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index bd3369447ba..47a28fd1114 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -64,7 +64,7 @@ class TestTask(unittest.TestCase): def assign(): from frappe.desk.form import assign_to assign_to.add({ - "assign_to": "test@example.com", + "assign_to": ["test@example.com"], "doctype": task.doctype, "name": task.name, "description": "Close this task" diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 32f0428fcd8..cbc624c064f 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -13,7 +13,7 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.hr.doctype.salary_structure.test_salary_structure \ import make_salary_structure, create_salary_structure_assignment - +from erpnext.hr.doctype.employee.test_employee import make_employee class TestTimesheet(unittest.TestCase): def setUp(self): @@ -25,8 +25,10 @@ class TestTimesheet(unittest.TestCase): def test_timesheet_billing_amount(self): - make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, billable=1) self.assertEqual(timesheet.total_hours, 2) self.assertEqual(timesheet.total_billable_hours, 2) @@ -35,8 +37,10 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.total_billable_amount, 100) def test_timesheet_billing_amount_not_billable(self): - make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=0) + emp = make_employee("test_employee_6@salary.com") + + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, billable=0) self.assertEqual(timesheet.total_hours, 2) self.assertEqual(timesheet.total_billable_hours, 0) @@ -45,8 +49,10 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.total_billable_amount, 0) def test_salary_slip_from_timesheet(self): - salary_structure = make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate = True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + salary_structure = make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate = True, billable=1) salary_slip = make_salary_slip(timesheet.name) salary_slip.submit() @@ -65,7 +71,9 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.status, 'Submitted') def test_sales_invoice_from_timesheet(self): - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + timesheet = make_timesheet(emp, simulate=True, billable=1) sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer') sales_invoice.due_date = nowdate() sales_invoice.submit() @@ -80,7 +88,9 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(item.rate, 50.00) def test_timesheet_billing_based_on_project(self): - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1, project = '_Test Project', company='_Test Company') + emp = make_employee("test_employee_6@salary.com") + + timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company') sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = '_Test Project' sales_invoice.submit() @@ -90,6 +100,8 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name) def test_timesheet_time_overlap(self): + emp = make_employee("test_employee_6@salary.com") + settings = frappe.get_single('Projects Settings') initial_setting = settings.ignore_employee_time_overlap settings.ignore_employee_time_overlap = 0 @@ -97,7 +109,7 @@ class TestTimesheet(unittest.TestCase): update_activity_type("_Test Activity Type") timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.append( 'time_logs', { @@ -129,12 +141,14 @@ class TestTimesheet(unittest.TestCase): settings.save() def test_timesheet_std_working_hours(self): + emp = make_employee("test_employee_6@salary.com") + company = frappe.get_doc('Company', "_Test Company") company.standard_working_hours = 8 company.save() timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.company = '_Test Company' timesheet.append( 'time_logs', @@ -156,7 +170,7 @@ class TestTimesheet(unittest.TestCase): company.save() timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.company = '_Test Company' timesheet.append( 'time_logs', diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 3eea390ff31..defc18bf4e9 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -258,7 +258,12 @@ var calculate_end_time = function(frm, cdt, cdn) { var update_billing_hours = function(frm, cdt, cdn){ var child = locals[cdt][cdn]; - if(!child.billable) frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); + if(!child.billable) { + frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); + } else { + // bill all hours by default + frappe.model.set_value(cdt, cdn, "billing_hours", child.hours); + } }; var update_time_rates = function(frm, cdt, cdn){ diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 9c561894764..a4cc68b3e2c 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -73,6 +73,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ me.frm.set_query('contact_person', erpnext.queries.contact_query); me.frm.set_query('supplier_address', erpnext.queries.address_query); + me.frm.set_query('billing_address', erpnext.queries.company_address_query); + if(this.frm.fields_dict.supplier) { this.frm.set_query("supplier", function() { return{ query: "erpnext.controllers.queries.supplier_query" }}); @@ -283,6 +285,11 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ "shipping_address_display", true); }, + billing_address: function() { + erpnext.utils.get_address_display(this.frm, "billing_address", + "billing_address_display", true); + }, + tc_name: function() { this.get_terms(); }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 524a95804fd..ca897dd4b17 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -552,7 +552,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if (show_batch_dialog) return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { - if(r.message.has_batch_no || r.message.has_serial_no) { + if (r.message && + (r.message.has_batch_no || r.message.has_serial_no)) { frappe.flags.hide_serial_batch_dialog = false; } }); @@ -917,7 +918,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ shipping_rule: function() { var me = this; - if(this.frm.doc.shipping_rule && this.frm.doc.shipping_address) { + if(this.frm.doc.shipping_rule) { return this.frm.call({ doc: this.frm.doc, method: "apply_shipping_rule", diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 2cd79b5d5e7..bcab0d84c62 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -487,7 +487,14 @@ erpnext.utils.update_child_items = function(opts) { fieldtype: 'Date', fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date", in_list_view: 1, - label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date") + label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date"), + reqd: 1 + }) + fields.splice(3, 0, { + fieldtype: 'Float', + fieldname: "conversion_factor", + in_list_view: 1, + label: __("Conversion Factor") }) } @@ -536,6 +543,7 @@ erpnext.utils.update_child_items = function(opts) { "item_code": d.item_code, "delivery_date": d.delivery_date, "schedule_date": d.schedule_date, + "conversion_factor": d.conversion_factor, "qty": d.qty, "rate": d.rate, }); diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index 735b417da17..617e916724d 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -81,4 +81,10 @@ .place-order-container { text-align: right; +} + +.kb-card { + .card-body > .card-title { + line-height: 1.3; + } } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index ded3a51dd60..cf2644e0053 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -2,4 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Quality Procedure', { + refresh: function(frm) { + frm.set_query("procedure","processes", (frm) =>{ + return { + filters: { + name: ["not in", [frm.parent_quality_procedure, frm.name]] + } + }; + }); + } }); \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index 6df116c430c..b3c0d948909 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_rename": 1, "autoname": "format:PRC-{quality_procedure_name}", "creation": "2018-10-06 00:06:29.756804", "doctype": "DocType", @@ -72,7 +73,7 @@ ], "is_tree": 1, "links": [], - "modified": "2020-03-18 18:09:29.371627", + "modified": "2020-06-17 17:25:03.434953", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index d29710dd8e3..1952e578673 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -11,18 +11,18 @@ class QualityProcedure(NestedSet): nsm_parent_field = 'parent_quality_procedure' def before_save(self): - for process in self.processes: - if process.procedure: - doc = frappe.get_doc("Quality Procedure", process.procedure) - if doc.parent_quality_procedure: - frappe.throw(_("{0} already has a Parent Procedure {1}.").format(process.procedure, doc.parent_quality_procedure)) - self.is_group = 1 + self.check_for_incorrect_child() def on_update(self): self.set_parent() def after_insert(self): self.set_parent() + #if Child is Added through Tree View. + if self.parent_quality_procedure: + parent_quality_procedure = frappe.get_doc("Quality Procedure", self.parent_quality_procedure) + parent_quality_procedure.append("processes", {"procedure": self.name}) + parent_quality_procedure.save() def on_trash(self): if self.parent_quality_procedure: @@ -42,11 +42,21 @@ class QualityProcedure(NestedSet): doc.save(ignore_permissions=True) def set_parent(self): + for process in self.processes: + # Set parent for only those children who don't have a parent + parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + if not parent_quality_procedure and process.procedure: + frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name) + + def check_for_incorrect_child(self): for process in self.processes: if process.procedure: - doc = frappe.get_doc("Quality Procedure", process.procedure) - doc.parent_quality_procedure = self.name - doc.save(ignore_permissions=True) + # Check if any child process belongs to another parent. + parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + if parent_quality_procedure and parent_quality_procedure != self.name: + frappe.throw(_("{0} already has a Parent Procedure {1}.".format(frappe.bold(process.procedure), frappe.bold(parent_quality_procedure))), + title=_("Invalid Child Procedure")) + self.is_group = 1 @frappe.whitelist() def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False): diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js index 6df6f656aa1..ef48ab6c6e2 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js @@ -16,6 +16,7 @@ frappe.treeview_settings["Quality Procedure"] = { }, ], breadcrumb: "Setup", + disable_add_node: true, root_label: "All Quality Procedures", get_tree_root: false, menu_items: [ diff --git a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json index 0a67fa505ee..3925dbb8aca 100644 --- a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json +++ b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json @@ -1,6 +1,8 @@ { + "actions": [], "creation": "2019-05-26 00:10:00.248885", "doctype": "DocType", + "editable_grid": 1, "engine": "InnoDB", "field_order": [ "process_description", @@ -23,7 +25,8 @@ } ], "istable": 1, - "modified": "2019-05-26 22:05:49.007189", + "links": [], + "modified": "2020-06-17 15:44:38.937915", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure Process", diff --git a/erpnext/regional/address_template/templates/india.html b/erpnext/regional/address_template/templates/india.html index ffb9d0547e0..5d2329efffa 100644 --- a/erpnext/regional/address_template/templates/india.html +++ b/erpnext/regional/address_template/templates/india.html @@ -1,7 +1,7 @@ {{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}{{ city }}
{% if gst_state %}{{ gst_state }}{% endif -%} {% if gst_state_number %}, State Code: {{ gst_state_number }}
{% endif -%} -{% if pincode %}PIN: {{ pincode }}
{% endif -%} +{% if pincode %}Postal Code: {{ pincode }}
{% endif -%} {{ country }}
{% if phone %}Phone: {{ phone }}
{% endif -%} {% if fax %}Fax: {{ fax }}
{% endif -%} diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html index 35f9cf674ce..888b2da48eb 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html @@ -52,7 +52,7 @@ - (d) {{__("Inward Supplies(liable to reverse charge")}} + (d) {{__("Inward Supplies(liable to reverse charge)")}} {{ flt(data.sup_details.isup_rev.txval, 2) }} {{ flt(data.sup_details.isup_rev.iamt, 2) }} {{ flt(data.sup_details.isup_rev.camt, 2) }} diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 9e7a023926d..619734ff263 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -158,7 +158,7 @@ class GSTR3BReport(Document): self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"]) self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"]) - self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Registered Regular"], reverse_charge="Y") + self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y") self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2) self.set_itc_details(itc_details) @@ -192,31 +192,27 @@ class GSTR3BReport(Document): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_type_map.get(d["ty"]) - gst_category = "Registered Regular" + gst_category = ["Registered Regular"] if d["ty"] == 'ISRC': reverse_charge = "Y" + itc_type = 'All Other ITC' + gst_category = ['Unregistered', 'Overseas'] else: reverse_charge = "N" for account_head in self.account_heads: + for category in gst_category: + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2) - d["iamt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('igst_account')), {}).get("amount"), 2) - d["camt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('cgst_account')), {}).get("amount"), 2) - d["samt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('sgst_account')), {}).get("amount"), 2) - d["csamt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('cess_account')), {}).get("amount"), 2) - - net_itc["iamt"] += flt(d["iamt"], 2) - net_itc["camt"] += flt(d["camt"], 2) - net_itc["samt"] += flt(d["samt"], 2) - net_itc["csamt"] += flt(d["csamt"], 2) + for key in ['iamt', 'camt', 'samt', 'csamt']: + net_itc[key] += flt(d[key], 2) for account_head in self.account_heads: itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1] - itc_inelg["iamt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("igst_account")), {}).get("amount"), 2) - itc_inelg["camt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("cgst_account")), {}).get("amount"), 2) - itc_inelg["samt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("sgst_account")), {}).get("amount"), 2) - itc_inelg["csamt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("cess_account")), {}).get("amount"), 2) + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2) def prepare_data(self, doctype, tax_details, supply_type, supply_category, gst_category_list, reverse_charge="N"): @@ -274,17 +270,16 @@ class GSTR3BReport(Document): """ #nosec .format(doctype = doctype), (self.month_no, self.year, reverse_charge, self.company, self.gst_details.get("gstin")))) - def get_itc_details(self, reverse_charge='N'): - + def get_itc_details(self): itc_amount = frappe.db.sql(""" select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t - where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s + where s.docstatus = 1 and t.parent = s.name and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s and s.company_gstin = %s group by t.account_head, s.gst_category, s.eligibility_for_itc """, - (reverse_charge, self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) itc_details = {} diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index 6b9567c0e57..31a7545a0df 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -64,7 +64,8 @@ class ImportSupplierInvoice(Document): "buying_price_list": self.default_buying_price_list } - if not invoices_args.get("invoice_no", ''): return + if not invoices_args.get("bill_no", ''): + frappe.throw(_("Numero has not set in the XML file")) supp_dict = get_supplier_details(file_content) invoices_args["destination_code"] = get_destination_code_from_file(file_content) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 732780a0a33..9fe29eba1b8 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -9,6 +9,8 @@ from erpnext.hr.utils import get_salary_assignment from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping from six import string_types +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.utils import get_account_currency def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -615,8 +617,9 @@ def get_transport_details(data, doc): data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy') if doc.gst_transporter_id: - validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') - data.transporterId = doc.gst_transporter_id + if doc.gst_transporter_id[0:2] != "88": + validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') + data.transporterId = doc.gst_transporter_id return data @@ -657,5 +660,53 @@ def get_gst_accounts(company, account_wise=False): elif val: gst_accounts[val] = acc - return gst_accounts + +def make_reverse_charge_entries(doc, method): + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'India': + return + + if doc.reverse_charge == 'Y': + gl_entries = [] + gst_accounts = get_gst_accounts(doc.company) + gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + + gst_accounts.get('igst_account') + + for tax in doc.get('taxes'): + if tax.category not in ("Total", "Valuation and Total"): + continue + + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: + account_currency = get_account_currency(tax.account_head) + + gl_entries.append(doc.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "posting_date": doc.posting_date, + "against": doc.supplier, + "credit": tax.base_tax_amount_after_discount_amount, + "credits_in_account_currency": tax.base_tax_amount_after_discount_amount \ + if account_currency==doc.company_currency \ + else tax.tax_amount_after_discount_amount + }, account_currency, item=tax) + ) + + gl_entries.append(doc.get_gl_dict( + { + "account": doc.credit_to if doc.doctype == 'Purchase Invoice' else doc.debit_to, + "cost_center": doc.cost_center, + "posting_date": doc.posting_date, + "party_type": 'Supplier', + "party": doc.supplier, + "against": tax.account_head, + "debit": tax.base_tax_amount_after_discount_amount, + "debit_in_account_currency": tax.base_tax_amount_after_discount_amount \ + if account_currency==doc.company_currency \ + else tax.tax_amount_after_discount_amount + }, account_currency, item=doc) + ) + + make_gl_entries(gl_entries) \ No newline at end of file diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index 6d344025d26..cae28bee8bc 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -9,14 +9,14 @@ def setup(company=None, patch=True): make_custom_fields() add_print_formats() -def make_custom_fields(): +def make_custom_fields(update=True): custom_fields = { 'Supplier': [ dict(fieldname='irs_1099', fieldtype='Check', insert_after='tax_id', label='Is IRS 1099 reporting required for supplier?') ] } - create_custom_fields(custom_fields) + create_custom_fields(custom_fields, update=update) def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") diff --git a/erpnext/selling/desk_page/selling/selling.json b/erpnext/selling/desk_page/selling/selling.json index c32a7c8481d..9ec634354d3 100644 --- a/erpnext/selling/desk_page/selling/selling.json +++ b/erpnext/selling/desk_page/selling/selling.json @@ -29,7 +29,7 @@ "category": "Modules", "charts": [ { - "chart_name": "Income", + "chart_name": "Incoming Bills (Purchase Invoice)", "label": "Income" } ], @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Selling", - "modified": "2020-05-28 13:46:08.314240", + "modified": "2020-06-03 13:23:24.861706", "modified_by": "Administrator", "module": "Selling", "name": "Selling", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index a6889e080d9..682dfede72f 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -3,16 +3,19 @@ from __future__ import unicode_literals import frappe +import json from frappe.model.naming import set_name_by_naming_series -from frappe import _, msgprint, throw +from frappe import _, msgprint import frappe.defaults -from frappe.utils import flt, cint, cstr, today +from frappe.utils import flt, cint, cstr, today, get_formatted_email from frappe.desk.reportview import build_match_conditions, get_filters_cond from erpnext.utilities.transaction_base import TransactionBase from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this from frappe.contacts.address_and_contact import load_address_and_contact, delete_contact_and_address from frappe.model.rename_doc import update_linked_doctypes from frappe.model.mapper import get_mapped_doc +from frappe.utils.user import get_users_with_role + class Customer(TransactionBase): def get_feed(self): @@ -336,6 +339,7 @@ def get_loyalty_programs(doc): return lp_details +@frappe.whitelist() def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): from erpnext.controllers.queries import get_fields @@ -378,10 +382,45 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, .format(customer, customer_outstanding, credit_limit)) # If not authorized person raise exception - credit_controller = frappe.db.get_value('Accounts Settings', None, 'credit_controller') - if not credit_controller or credit_controller not in frappe.get_roles(): - throw(_("Please contact to the user who have Sales Master Manager {0} role") - .format(" / " + credit_controller if credit_controller else "")) + credit_controller_role = frappe.db.get_single_value('Accounts Settings', 'credit_controller') + if not credit_controller_role or credit_controller_role not in frappe.get_roles(): + # form a list of emails for the credit controller users + credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") + + # form a list of emails and names to show to the user + credit_controller_users_list = [user for user in credit_controller_users if frappe.db.exists("Employee", {"prefered_email": user})] + credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users_list] + + if not credit_controller_users: + frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) + + message = """Please contact any of the following users to extend the credit limits for {0}: +

""".format(customer, '
  • '.join(credit_controller_users)) + + # if the current user does not have permissions to override credit limit, + # prompt them to send out an email to the controller users + frappe.msgprint(message, + title="Notify", + raise_exception=1, + primary_action={ + 'label': 'Send Email', + 'server_action': 'erpnext.selling.doctype.customer.customer.send_emails', + 'args': { + 'customer': customer, + 'customer_outstanding': customer_outstanding, + 'credit_limit': credit_limit, + 'credit_controller_users_list': credit_controller_users_list + } + } + ) + +@frappe.whitelist() +def send_emails(args): + args = json.loads(args) + subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) + message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") + .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) + frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message) def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 05e4aa892b0..ffb66354fa0 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -868,7 +868,8 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe ], "field_no_map": [ "rate", - "price_list_rate" + "price_list_rate", + "item_tax_template" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.qty and doc.supplier == supplier and doc.item_code in selected_items diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b8b0d404e50..74e742fabbb 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -400,6 +400,23 @@ class TestSalesOrder(unittest.TestCase): trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + def test_update_child_qty_rate_perm(self): + so = make_sales_order(item_code= "_Test Item", qty=4) + + user = 'test@example.com' + test_user = frappe.get_doc('User', user) + test_user.add_roles("Accounts User") + frappe.set_user(user) + + # update qty + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + + # add new item + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + frappe.set_user("Administrator") + def test_warehouse_user(self): frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index e59349926e6..eff17f8bc78 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -769,22 +769,23 @@ "label": "Against Blanket Order" }, { - "fieldname": "bom_no", - "fieldtype": "Link", - "label": "BOM No", - "no_copy": 1, - "options": "BOM", - "print_hide": 1 - }, - { - "fieldname": "manufacturing_section_section", - "fieldtype": "Section Break", - "label": "Manufacturing Section" - } + "fieldname": "bom_no", + "fieldtype": "Link", + "label": "BOM No", + "no_copy": 1, + "options": "BOM", + "print_hide": 1 + }, + { + "fieldname": "manufacturing_section_section", + "fieldtype": "Section Break", + "label": "Manufacturing Section" + } ], "idx": 1, "istable": 1, - "modified": "2020-05-15 18:13:43.006493", + "links": [], + "modified": "2020-05-29 20:54:32.309460", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 88bd9c135d5..f15f63d7bb7 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -5,183 +5,183 @@ from __future__ import unicode_literals import calendar import frappe from frappe import _ -from frappe.utils import cint, cstr +from frappe.utils import cint, cstr, getdate def execute(filters=None): - common_columns = [ - { - 'label': _('New Customers'), - 'fieldname': 'new_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 - }, - { - 'label': _('Repeat Customers'), - 'fieldname': 'repeat_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 - }, - { - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'default': 0, - 'width': 100 - }, - { - 'label': _('New Customer Revenue'), - 'fieldname': 'new_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - }, - { - 'label': _('Repeat Customer Revenue'), - 'fieldname': 'repeat_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - }, - { - 'label': _('Total Revenue'), - 'fieldname': 'total_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - } - ] - if filters.get('view_type') == 'Monthly': - return get_data_by_time(filters, common_columns) - else: - return get_data_by_territory(filters, common_columns) + common_columns = [ + { + 'label': _('New Customers'), + 'fieldname': 'new_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 125 + }, + { + 'label': _('Repeat Customers'), + 'fieldname': 'repeat_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 125 + }, + { + 'label': _('Total'), + 'fieldname': 'total', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('New Customer Revenue'), + 'fieldname': 'new_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 175 + }, + { + 'label': _('Repeat Customer Revenue'), + 'fieldname': 'repeat_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 175 + }, + { + 'label': _('Total Revenue'), + 'fieldname': 'total_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 175 + } + ] + if filters.get('view_type') == 'Monthly': + return get_data_by_time(filters, common_columns) + else: + return get_data_by_territory(filters, common_columns) def get_data_by_time(filters, common_columns): - # key yyyy-mm - columns = [ - { - 'label': _('Year'), - 'fieldname': 'year', - 'fieldtype': 'Data', - 'width': 100 - }, - { - 'label': _('Month'), - 'fieldname': 'month', - 'fieldtype': 'Data', - 'width': 100 - }, - ] - columns += common_columns + # key yyyy-mm + columns = [ + { + 'label': _('Year'), + 'fieldname': 'year', + 'fieldtype': 'Data', + 'width': 100 + }, + { + 'label': _('Month'), + 'fieldname': 'month', + 'fieldtype': 'Data', + 'width': 100 + }, + ] + columns += common_columns - customers_in = get_customer_stats(filters) + customers_in = get_customer_stats(filters) - # time series - from_year, from_month, temp = filters.get('from_date').split('-') - to_year, to_month, temp = filters.get('to_date').split('-') + # time series + from_year, from_month, temp = filters.get('from_date').split('-') + to_year, to_month, temp = filters.get('to_date').split('-') - from_year, from_month, to_year, to_month = \ - cint(from_year), cint(from_month), cint(to_year), cint(to_month) + from_year, from_month, to_year, to_month = \ + cint(from_year), cint(from_month), cint(to_year), cint(to_month) - out = [] - for year in range(from_year, to_year+1): - for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): - key = '{year}-{month:02d}'.format(year=year, month=month) - data = customers_in.get(key) - new = data['new'] if data else [0, 0.0] - repeat = data['repeat'] if data else [0, 0.0] - out.append({ - 'year': cstr(year), - 'month': calendar.month_name[month], - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1] - }) - return columns, out + out = [] + for year in range(from_year, to_year+1): + for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): + key = '{year}-{month:02d}'.format(year=year, month=month) + data = customers_in.get(key) + new = data['new'] if data else [0, 0.0] + repeat = data['repeat'] if data else [0, 0.0] + out.append({ + 'year': cstr(year), + 'month': calendar.month_name[month], + 'new_customers': new[0], + 'repeat_customers': repeat[0], + 'total': new[0] + repeat[0], + 'new_customer_revenue': new[1], + 'repeat_customer_revenue': repeat[1], + 'total_revenue': new[1] + repeat[1] + }) + return columns, out def get_data_by_territory(filters, common_columns): - columns = [{ - 'label': 'Territory', - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 150 - }] - columns += common_columns + columns = [{ + 'label': 'Territory', + 'fieldname': 'territory', + 'fieldtype': 'Link', + 'options': 'Territory', + 'width': 150 + }] + columns += common_columns - customers_in = get_customer_stats(filters, tree_view=True) + customers_in = get_customer_stats(filters, tree_view=True) - territory_dict = {} - for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): - territory_dict.update({ - t.name: { - 'parent': t.parent_territory, - 'is_group': t.is_group - } - }) + territory_dict = {} + for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): + territory_dict.update({ + t.name: { + 'parent': t.parent_territory, + 'is_group': t.is_group + } + }) - depth_map = frappe._dict() - for name, info in territory_dict.items(): - default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 - depth_map.setdefault(name, default) + depth_map = frappe._dict() + for name, info in territory_dict.items(): + default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 + depth_map.setdefault(name, default) - data = [] - for name, indent in depth_map.items(): - condition = customers_in.get(name) - new = customers_in[name]['new'] if condition else [0, 0.0] - repeat = customers_in[name]['repeat'] if condition else [0, 0.0] - temp = { - 'territory': name, - 'parent_territory': territory_dict[name]['parent'], - 'indent': indent, - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1], - 'bold': 0 if indent else 1 - } - data.append(temp) + data = [] + for name, indent in depth_map.items(): + condition = customers_in.get(name) + new = customers_in[name]['new'] if condition else [0, 0.0] + repeat = customers_in[name]['repeat'] if condition else [0, 0.0] + temp = { + 'territory': name, + 'parent_territory': territory_dict[name]['parent'], + 'indent': indent, + 'new_customers': new[0], + 'repeat_customers': repeat[0], + 'total': new[0] + repeat[0], + 'new_customer_revenue': new[1], + 'repeat_customer_revenue': repeat[1], + 'total_revenue': new[1] + repeat[1], + 'bold': 0 if indent else 1 + } + data.append(temp) - loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) + loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) - for ld in loop_data: - if ld['parent_territory']: - parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] - for key in parent_data.keys(): - if key not in ['indent', 'territory', 'parent_territory', 'bold']: - parent_data[key] += ld[key] + for ld in loop_data: + if ld['parent_territory']: + parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] + for key in parent_data.keys(): + if key not in ['indent', 'territory', 'parent_territory', 'bold']: + parent_data[key] += ld[key] - return columns, data, None, None, None, 1 + return columns, data, None, None, None, 1 def get_customer_stats(filters, tree_view=False): - """ Calculates number of new and repeated customers. """ - company_condition = '' - if filters.get('company'): - company_condition = ' and company=%(company)s' + """ Calculates number of new and repeated customers and revenue. """ + company_condition = '' + if filters.get('company'): + company_condition = ' and company=%(company)s' - customers = [] - customers_in = {} + customers = [] + customers_in = {} - for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` - where docstatus=1 and posting_date <= %(to_date)s and posting_date >= %(from_date)s - {company_condition} order by posting_date'''.format(company_condition=company_condition), - filters, as_dict=1): + for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + where docstatus=1 and posting_date <= %(to_date)s + {company_condition} order by posting_date'''.format(company_condition=company_condition), + filters, as_dict=1): - key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') - customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) + key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') + new_or_repeat = 'new' if si.customer not in customers else 'repeat' + customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) - if not si.customer in customers: - customers_in[key]['new'][0] += 1 - customers_in[key]['new'][1] += si.base_grand_total - customers.append(si.customer) - else: - customers_in[key]['repeat'][0] += 1 - customers_in[key]['repeat'][1] += si.base_grand_total + # if filters.from_date <= si.posting_date.strftime('%Y-%m-%d'): + if getdate(filters.from_date) <= getdate(si.posting_date): + customers_in[key][new_or_repeat][0] += 1 + customers_in[key][new_or_repeat][1] += si.base_grand_total + if new_or_repeat == 'new': + customers.append(si.customer) - return customers_in + return customers_in diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 405004ece54..08a98ba6c07 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -96,7 +96,7 @@ def get_columns(filters): "label": _("Customer Group"), "fieldtype": "Link", "fieldname": "customer_group", - "options": "customer Group", + "options": "Customer Group", "width": 120 }, { diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 4d2d540bbc8..7c0be3bff63 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -101,8 +101,7 @@ class EmailDigest(Document): if not context.purchase_order_list: frappe.throw(_("No items to be received are overdue")) - if not (context.events or context.todo_list or context.notifications or context.cards - or context.purchase_orders_items_overdue_list): + if not context: return None frappe.flags.ignore_account_permission = False diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index aba6a791a4e..28d1d16a051 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:title", @@ -49,7 +50,7 @@ "fieldname": "terms_and_conditions_help", "fieldtype": "HTML", "label": "Terms and Conditions Help", - "options": "

    Standard Terms and Conditions Example

    \n\n
    Delivery Terms for Order number {{ name }}\n\n-Order Date : {{ transaction_date }} \n-Expected Delivery Date : {{ delivery_date }}\n
    \n\n

    How to get fieldnames

    \n\n

    The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

    \n\n

    Templating

    \n\n

    Templates are compiled using the Jinja Templating Langauge. To learn more about Jinja, read this documentation.

    " + "options": "

    Standard Terms and Conditions Example

    \n\n
    Delivery Terms for Order number {{ name }}\n\n-Order Date : {{ transaction_date }} \n-Expected Delivery Date : {{ delivery_date }}\n
    \n\n

    How to get fieldnames

    \n\n

    The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

    \n\n

    Templating

    \n\n

    Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

    " }, { "fieldname": "applicable_modules_section", @@ -81,7 +82,8 @@ ], "icon": "icon-legal", "idx": 1, - "modified": "2019-07-04 13:31:30.393425", + "links": [], + "modified": "2020-06-16 22:54:38.094844", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index e666a41f300..74ff0ecfd8f 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -105,3 +105,4 @@ def add_company_to_session_defaults(): "ref_doctype": "Company" }) settings.save() + diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index d04c8c25a34..a7e8388be9d 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -42,9 +42,9 @@ def get_cart_quotation(doc=None): return { "doc": decorate_quotation_doc(doc), - "shipping_addresses": [{"name": address.name, "display": address.display} + "shipping_addresses": [{"name": address.name, "title": address.address_title, "display": address.display} for address in addresses if address.address_type == "Shipping"], - "billing_addresses": [{"name": address.name, "display": address.display} + "billing_addresses": [{"name": address.name, "title": address.address_title, "display": address.display} for address in addresses if address.address_type == "Billing"], "shipping_rules": get_applicable_shipping_rules(party), "cart_settings": frappe.get_cached_doc("Shopping Cart Settings") @@ -78,8 +78,10 @@ def place_order(): if is_stock_item: item_stock = get_qty_in_stock(item.item_code, "website_warehouse") + if not cint(item_stock.in_stock): + throw(_("{1} Not in Stock").format(item.item_code)) if item.qty > item_stock.stock_qty[0][0]: - throw(_("Only {0} in stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) + throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) sales_order.flags.ignore_permissions = True sales_order.insert() @@ -337,21 +339,17 @@ def set_price_list_and_rate(quotation, cart_settings): def _set_price_list(cart_settings, quotation=None): """Set price list based on customer or shopping cart default""" from erpnext.accounts.party import get_default_price_list - - # check if customer price list exists + party_name = quotation.get("party_name") if quotation else get_party().get("name") selling_price_list = None - if quotation and quotation.get("party_name"): - selling_price_list = frappe.db.get_value('Customer', quotation.get("party_name"), 'default_price_list') - # else check for territory based price list + # check if default customer price list exists + if party_name: + selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) + + # check default price list in shopping cart if not selling_price_list: selling_price_list = cart_settings.price_list - party_name = quotation.get("party_name") if quotation else get_party().get("name") - - if not selling_price_list and party_name: - selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) - if quotation: quotation.selling_price_list = selling_price_list diff --git a/erpnext/startup/filters.py b/erpnext/startup/filters.py new file mode 100644 index 00000000000..a99e49b4917 --- /dev/null +++ b/erpnext/startup/filters.py @@ -0,0 +1,14 @@ + +import frappe + +def get_filters_config(): + filters_config = { + "fiscal year": { + "label": "Fiscal Year", + "get_field": "erpnext.accounts.utils.get_fiscal_year_filter_field", + "valid_for_fieldtypes": ["Date", "Datetime", "DateRange"], + "depends_on": "company", + } + } + + return filters_config \ No newline at end of file diff --git a/erpnext/stock/dashboard_fixtures.py b/erpnext/stock/dashboard_fixtures.py index 0f1fd128f0b..7625b1ad283 100644 --- a/erpnext/stock/dashboard_fixtures.py +++ b/erpnext/stock/dashboard_fixtures.py @@ -5,31 +5,26 @@ import frappe import json from frappe import _ from frappe.utils import nowdate -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year +from erpnext.buying.dashboard_fixtures import get_company_for_dashboards def get_data(): + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + company = frappe.get_doc("Company", get_company_for_dashboards()) + fiscal_year_name = fiscal_year.get("name") + start_date = str(fiscal_year.get("year_start_date")) + end_date = str(fiscal_year.get("year_end_date")) + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), + "charts": get_charts(company, fiscal_year_name, start_date, end_date), + "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date), }) -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 - -company = frappe.get_doc("Company", get_company_for_dashboards()) -fiscal_year = get_fiscal_year(nowdate(), as_dict=1) -fiscal_year_name = fiscal_year.get("name") -start_date = str(fiscal_year.get("year_start_date")) -end_date = str(fiscal_year.get("year_end_date")) - def get_dashboards(): return [{ "name": "Stock", @@ -48,7 +43,7 @@ def get_dashboards(): ] }] -def get_charts(): +def get_charts(company, fiscal_year_name, start_date, end_date): return [ { "doctype": "Dashboard Chart", @@ -133,7 +128,7 @@ def get_charts(): } ] -def get_number_cards(): +def get_number_cards(company, fiscal_year_name, start_date, end_date): return [ { "name": "Total Active Items", diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 71b998fb954..2f75bbd97c0 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -34,7 +34,7 @@ class ItemAttribute(Document): if self.numeric_values: validate_is_incremental(self, self.name, item.value, item.name) else: - validate_item_attribute_value(attributes_list, self.name, item.value, item.name) + validate_item_attribute_value(attributes_list, self.name, item.value, item.name, from_variant=False) def validate_numeric(self): if self.numeric_values: diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 3562181e25f..3a8deb6d254 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -18,7 +18,7 @@ frappe.ui.form.on('Material Request', { // formatter for material request item frm.set_indicator_formatter('item_code', - function(doc) { return (doc.qty<=doc.ordered_qty) ? "green" : "orange"; }); + function(doc) { return (doc.stock_qty<=doc.ordered_qty) ? "green" : "orange"; }); frm.set_query("item_code", "items", function() { return { diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 93b29c8daff..4b8b594ed9d 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -119,11 +119,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map): if item_location.serial_no: serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)]) + auto_set_serial_no = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo") + locations.append(frappe._dict({ 'qty': qty, 'stock_qty': stock_qty, 'warehouse': item_location.warehouse, - 'serial_no': serial_nos, + 'serial_no': serial_nos if auto_set_serial_no else item_doc.serial_no, 'batch_no': item_location.batch_no })) @@ -206,6 +208,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re sle.batch_no = batch.name and sle.`item_code`=%(item_code)s and sle.`company` = %(company)s + and batch.disabled = 0 and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY @@ -471,4 +474,4 @@ def update_common_item_properties(item, location): item.material_request = location.material_request item.serial_no = location.serial_no item.batch_no = location.batch_no - item.material_request_item = location.material_request_item \ No newline at end of file + item.material_request_item = location.material_request_item diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index e9568eeacc0..50c18f62824 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -25,7 +25,7 @@ frappe.ui.form.on("Purchase Receipt", { frm.custom_make_buttons = { 'Stock Entry': 'Return', - 'Purchase Invoice': 'Invoice' + 'Purchase Invoice': 'Purchase Invoice' }; frm.set_query("expense_account", "items", function() { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 02f72e2e394..3b92dac220e 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -31,6 +31,8 @@ "col_break_address", "shipping_address", "shipping_address_display", + "billing_address", + "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -130,13 +132,17 @@ { "fieldname": "supplier_section", "fieldtype": "Section Break", - "options": "fa fa-user" + "options": "fa fa-user", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break0", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -147,7 +153,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -159,7 +167,9 @@ "options": "MAT-PRE-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -174,6 +184,8 @@ "print_width": "150px", "reqd": 1, "search_index": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -184,18 +196,24 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Supplier Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_delivery_note", "fieldtype": "Data", - "label": "Supplier Delivery Note" + "label": "Supplier Delivery Note", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -210,6 +228,8 @@ "print_width": "100px", "reqd": 1, "search_index": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -223,6 +243,8 @@ "print_hide": 1, "print_width": "100px", "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -231,7 +253,9 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -245,6 +269,8 @@ "print_width": "150px", "remember_last_selected_value": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -254,7 +280,9 @@ "label": "Is Return", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_return", @@ -264,46 +292,60 @@ "no_copy": 1, "options": "Purchase Receipt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "in_global_search": 1, "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", @@ -311,32 +353,42 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -346,7 +398,9 @@ "oldfieldtype": "Select", "options": "Currency", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Rate at which supplier's currency is converted to company's base currency", @@ -357,13 +411,17 @@ "oldfieldtype": "Currency", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -371,7 +429,9 @@ "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "buying_price_list", @@ -380,7 +440,9 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "buying_price_list", @@ -388,7 +450,9 @@ "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -397,11 +461,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Sets 'Accepted Warehouse' in each row of the items table.", @@ -409,7 +477,9 @@ "fieldtype": "Link", "label": "Accepted Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Sets 'Rejected Warehouse' in each row of the items table.", @@ -420,11 +490,15 @@ "oldfieldname": "rejected_warehouse", "oldfieldtype": "Link", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -434,7 +508,9 @@ "oldfieldname": "is_subcontracted", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_subcontracted==\"Yes\"", @@ -447,13 +523,17 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -463,20 +543,26 @@ "oldfieldname": "purchase_receipt_details", "oldfieldtype": "Table", "options": "Purchase Receipt Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "supplied_items", @@ -485,7 +571,9 @@ "label": "Get Current Stock", "oldfieldtype": "Button", "options": "get_current_stock", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -496,7 +584,9 @@ "oldfieldtype": "Section Break", "options": "fa fa-table", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplied_items", @@ -507,18 +597,24 @@ "oldfieldtype": "Table", "options": "Purchase Receipt Item Supplied", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break0", "fieldtype": "Section Break", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -526,7 +622,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -539,18 +637,24 @@ "print_width": "150px", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { "fieldname": "column_break_27", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -560,42 +664,56 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Add / Edit Taxes and Charges", "fieldname": "taxes_charges_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_col", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", - "options": "Shipping Rule" + "options": "Shipping Rule", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -604,7 +722,9 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -612,13 +732,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -627,13 +751,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -643,7 +771,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -653,7 +783,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -663,12 +795,16 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break3", "fieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -679,7 +815,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -689,7 +827,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -697,14 +837,18 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_42", "fieldtype": "Section Break", - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -712,7 +856,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -720,28 +866,38 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_44", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_46", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -751,7 +907,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounding_adjustment", @@ -760,7 +918,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_in_words", @@ -769,7 +929,9 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounded_total", @@ -779,11 +941,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_50", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "grand_total", @@ -793,7 +959,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounding_adjustment", @@ -802,7 +970,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -812,7 +982,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -821,13 +993,17 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -836,7 +1012,9 @@ "fieldtype": "Section Break", "label": "Terms and Conditions", "oldfieldtype": "Section Break", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", @@ -845,14 +1023,18 @@ "oldfieldname": "tc_name", "oldfieldtype": "Link", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", "label": "Terms and Conditions", "oldfieldname": "terms", - "oldfieldtype": "Text Editor" + "oldfieldtype": "Text Editor", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_no", @@ -861,7 +1043,9 @@ "label": "Bill No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_date", @@ -870,7 +1054,9 @@ "label": "Bill Date", "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -878,7 +1064,9 @@ "fieldtype": "Section Break", "label": "More Information", "oldfieldtype": "Section Break", - "options": "fa fa-file-text" + "options": "fa fa-file-text", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -895,6 +1083,8 @@ "read_only": 1, "reqd": 1, "search_index": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -910,6 +1100,8 @@ "print_hide": 1, "print_width": "150px", "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -919,7 +1111,9 @@ "label": "Range", "oldfieldname": "range", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break4", @@ -927,6 +1121,8 @@ "oldfieldtype": "Column Break", "print_hide": 1, "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -941,12 +1137,16 @@ "label": "% Amount Billed", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "subscription_detail", "fieldtype": "Section Break", - "label": "Auto Repeat Detail" + "label": "Auto Repeat Detail", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -955,13 +1155,17 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings" + "label": "Printing Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -969,7 +1173,9 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -981,13 +1187,17 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -995,11 +1205,15 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_97", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_details", @@ -1010,6 +1224,8 @@ "options": "
    Other Details
    ", "print_hide": 1, "print_width": "30%", + "show_days": 1, + "show_seconds": 1, "width": "30%" }, { @@ -1017,13 +1233,17 @@ "fieldtype": "Small Text", "label": "Instructions", "oldfieldname": "instructions", - "oldfieldtype": "Text" + "oldfieldtype": "Text", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "remarks", "fieldtype": "Small Text", "label": "Remarks", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1031,19 +1251,25 @@ "fieldname": "transporter_info", "fieldtype": "Section Break", "label": "Transporter Details", - "options": "fa fa-truck" + "options": "fa fa-truck", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "transporter_name", "fieldtype": "Data", "label": "Transporter Name", "oldfieldname": "transporter_name", - "oldfieldtype": "Data" + "oldfieldtype": "Data", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break5", "fieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -1054,6 +1280,8 @@ "oldfieldname": "lr_no", "oldfieldtype": "Data", "print_width": "100px", + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -1064,6 +1292,8 @@ "oldfieldname": "lr_date", "oldfieldtype": "Date", "print_width": "100px", + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -1072,26 +1302,48 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_reference", "fieldtype": "Link", "label": "Inter Company Reference", "options": "Delivery Note", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-04-18 18:02:18.020763", + "modified": "2020-06-13 22:26:03.600092", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", @@ -1158,4 +1410,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 18d68539daa..5fbd512bf4f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -500,7 +500,7 @@ class StockEntry(StockController): 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: + elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) d.basic_amount = d.basic_rate * d.qty diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index c16a41c24fa..7b9c129804e 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -23,6 +23,7 @@ "image", "image_view", "quantity_and_rate", + "set_basic_rate_manually", "qty", "basic_rate", "basic_amount", @@ -491,12 +492,21 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", + "fieldname": "set_basic_rate_manually", + "fieldtype": "Check", + "label": "Set Basic Rate Manually", + "show_days": 1, + "show_seconds": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-23 19:19:28.539769", + "modified": "2020-06-08 12:57:03.172887", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 9a972104a27..5df3fa8067b 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -8,7 +8,7 @@ from frappe.utils import getdate, flt def execute(filters=None): if not filters: filters = {} - float_preceision = frappe.db.get_default("float_preceision") + float_precision = frappe.db.get_default("float_precision") condition = get_condition(filters) @@ -25,7 +25,7 @@ def execute(filters=None): data = [] for item in items: total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name,0)) - avg_daily_outgoing = flt(total_outgoing / diff, float_preceision) + avg_daily_outgoing = flt(total_outgoing / diff, float_precision) reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock) data.append([item.name, item.item_name, item.item_group, item.brand, item.description, diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index af997801551..723ed5c1c46 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -180,10 +180,10 @@ def get_fifo_queue(filters, sle=None): qty_to_pop = abs(d.actual_qty) while qty_to_pop: batch = fifo_queue[0] if fifo_queue else [0, None] - if 0 < batch[0] <= qty_to_pop: + if 0 < flt(batch[0]) <= qty_to_pop: # if batch qty > 0 # not enough or exactly same qty in current batch, clear batch - qty_to_pop -= batch[0] + qty_to_pop -= flt(batch[0]) transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) else: # all from current batch @@ -262,4 +262,4 @@ def get_chart_data(data, filters): ] }, "type" : "bar" - } \ No newline at end of file + } diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f21dc3f8b03..11e758fce32 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -230,12 +230,12 @@ def get_valuation_method(item_code): def get_fifo_rate(previous_stock_queue, qty): """get FIFO (average) Rate from Queue""" - if qty >= 0: + if flt(qty) >= 0: total = sum(f[0] for f in previous_stock_queue) return sum(flt(f[0]) * flt(f[1]) for f in previous_stock_queue) / flt(total) if total else 0.0 else: available_qty_for_outgoing, outgoing_cost = 0, 0 - qty_to_pop = abs(qty) + qty_to_pop = abs(flt(qty)) while qty_to_pop and previous_stock_queue: batch = previous_stock_queue[0] if 0 < batch[0] <= qty_to_pop: diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index a3fe72d0519..b1ad7c8aa0a 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Service Level Agreement", - "links": "[\n {\n \"description\": \"Service Level.\",\n \"label\": \"Service Level\",\n \"name\": \"Service Level\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-05-28 13:51:23.869954", + "modified": "2020-06-04 11:54:56.124219", "modified_by": "Administrator", "module": "Support", "name": "Support", @@ -65,8 +65,8 @@ "type": "DocType" }, { - "label": "Service Level", - "link_to": "Service Level", + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", "type": "DocType" } ] diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index bad40cc37fc..e7e5bd312bc 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -38,10 +38,35 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { if (frm.doc.service_level_agreement) { - set_time_to_resolve_and_response(frm); + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
    ' + + '
    ' + + ''+ message.msg +' ' + + '
    ' + + '
    ' + ); + } else { + set_time_to_resolve_and_response(frm); + } + } + }); } frm.add_custom_button(__("Close"), function () { @@ -55,6 +80,7 @@ frappe.ui.form.on("Issue", { frm: frm }); }, __("Make")); + } else { if (frm.doc.service_level_agreement) { frm.dashboard.clear_headline(); diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index c12cef4a5f3..6525ab27d30 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -31,9 +31,13 @@ "resolution_by", "resolution_by_variance", "service_level_agreement_creation", + "on_hold_since", + "total_hold_time", "response", "mins_to_first_response", "first_responded_on", + "column_break_26", + "avg_response_time", "additional_info", "lead", "contact", @@ -50,7 +54,9 @@ "resolution_date", "content_type", "attachment", - "via_customer_portal" + "via_customer_portal", + "resolution_time", + "user_resolution_time" ], "fields": [ { @@ -114,7 +120,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nHold\nClosed", + "options": "Open\nReplied\nHold\nResolved\nClosed", "search_index": 1 }, { @@ -161,6 +167,7 @@ "options": "Service Level Agreement" }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", @@ -174,6 +181,7 @@ "read_only": 1 }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", @@ -328,7 +336,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.service_level_agreement", + "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", "description": "in hours", "fieldname": "response_by_variance", "fieldtype": "Float", @@ -336,7 +344,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.service_level_agreement", + "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", "description": "in hours", "fieldname": "resolution_by_variance", "fieldtype": "Float", @@ -362,12 +370,48 @@ "label": "Issue Split From", "options": "Issue", "read_only": 1 + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "fieldname": "avg_response_time", + "fieldtype": "Duration", + "label": "Average Response Time", + "read_only": 1 + }, + { + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 + }, + { + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 + }, + { + "fieldname": "on_hold_since", + "fieldtype": "Datetime", + "hidden": 1, + "label": "On Hold Since", + "read_only": 1 + }, + { + "fieldname": "total_hold_time", + "fieldtype": "Duration", + "label": "Total Hold Time", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-03-13 02:19:49.477928", + "modified": "2020-06-10 12:47:37.146914", "modified_by": "Administrator", "module": "Support", "name": "Issue", @@ -395,4 +439,4 @@ "title_field": "subject", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 117267f1a42..883e603fd31 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ import json from frappe import _ from frappe import utils from frappe.model.document import Document -from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime +from frappe.utils import time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime, time_diff_in_seconds, time_diff from datetime import datetime, timedelta from frappe.model.mapper import get_mapped_doc from frappe.utils.user import is_website_user @@ -47,8 +47,8 @@ class Issue(Document): self.contact = frappe.db.get_value("Contact", {"email_id": email_id}) if self.contact: - contact = frappe.get_doc('Contact', self.contact) - self.customer = contact.get_link_for('Customer') + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") if not self.company: self.company = frappe.db.get_value("Lead", self.lead, "company") or \ @@ -56,18 +56,70 @@ class Issue(Document): def update_status(self): status = frappe.db.get_value("Issue", self.name, "status") - if self.status!="Open" and status =="Open" and not self.first_responded_on: + if self.status != "Open" and status == "Open" and not self.first_responded_on: self.first_responded_on = frappe.flags.current_time or now_datetime() - if self.status=="Closed" and status !="Closed": + if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: self.resolution_date = frappe.flags.current_time or now_datetime() if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": set_service_level_agreement_variance(issue=self.name) self.update_agreement_status() + set_resolution_time(issue=self) + set_user_resolution_time(issue=self) - if self.status=="Open" and status !="Open": + if self.status == "Open" and status != "Open": # if no date, it should be set as None and not a blank string "", as per mysql strict config self.resolution_date = None + self.reset_issue_metrics() + # enable SLA and variance on Reopen + self.agreement_fulfilled = "Ongoing" + set_service_level_agreement_variance(issue=self.name) + + self.handle_hold_time(status) + + def handle_hold_time(self, status): + if self.service_level_agreement: + # set response and resolution variance as None as the issue is on Hold for status as Replied + pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], + filters={"parent": self.service_level_agreement}) + hold_statuses = [entry.status for entry in pause_sla_on] + update_values = {} + + if self.status in hold_statuses and status not in hold_statuses: + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + if not self.first_responded_on: + update_values['response_by'] = None + update_values['response_by_variance'] = 0 + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 + + # calculate hold time when status is changed from Replied to any other status + if self.status not in hold_statuses and status in hold_statuses: + hold_time = self.total_hold_time if self.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime() + update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, self.on_hold_since) + + # re-calculate SLA variables after issue changes from Replied to Open + # add hold time to SLA variables + if self.status == "Open" and status in hold_statuses: + start_date_time = get_datetime(self.service_level_agreement_creation) + priority = get_priority(self) + now_time = frappe.flags.current_time or now_datetime() + hold_time = time_diff_in_seconds(now_time, self.on_hold_since) + + if not self.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time)) + response_by_variance = round(time_diff_in_hours(self.response_by, now_time)) + update_values['response_by_variance'] = response_by_variance + (hold_time // 3600) + + resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time)) + resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time)) + update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600) + update_values['on_hold_since'] = None + + self.db_set(update_values) def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": @@ -128,6 +180,7 @@ class Issue(Document): replicated_issue.response_by_variance = None replicated_issue.resolution_by = None replicated_issue.resolution_by_variance = None + replicated_issue.reset_issue_metrics() frappe.get_doc(replicated_issue).insert() @@ -137,7 +190,7 @@ class Issue(Document): communications = frappe.get_all("Communication", filters={"reference_doctype": "Issue", "reference_name": comm_to_split_from.reference_name, - "creation": ('>=', comm_to_split_from.creation)}) + "creation": (">=", comm_to_split_from.creation)}) for communication in communications: doc = frappe.get_doc("Communication", communication.name) @@ -173,20 +226,15 @@ class Issue(Document): self.service_level_agreement = service_level_agreement.name self.priority = service_level_agreement.default_priority if not priority else priority - service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement.name) - priority = service_level_agreement.get_service_level_agreement_priority(self.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) + priority = get_priority(self) if not self.creation: self.creation = now_datetime() self.service_level_agreement_creation = now_datetime() start_date_time = get_datetime(self.service_level_agreement_creation) - self.response_by = get_expected_time_for(parameter='response', service_level=priority, start_date_time=start_date_time) - self.resolution_by = get_expected_time_for(parameter='resolution', service_level=priority, start_date_time=start_date_time) + self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) @@ -221,36 +269,41 @@ class Issue(Document): self.agreement_fulfilled = "Ongoing" self.save() + def reset_issue_metrics(self): + self.db_set("resolution_time", None) + self.db_set("user_resolution_time", None) + + +def get_priority(issue): + service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement) + priority = service_level_agreement.get_service_level_agreement_priority(issue.priority) + priority.update({ + "support_and_resolution": service_level_agreement.support_and_resolution, + "holiday_list": service_level_agreement.holiday_list + }) + return priority + + def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time expected_time = current_date_time start_time = None end_time = None - # lets assume response time is in days by default - if parameter == 'response': - allotted_days = service_level.get("response_time") - time_period = service_level.get("response_time_period") - elif parameter == 'resolution': - allotted_days = service_level.get("resolution_time") - time_period = service_level.get("resolution_time_period") + if parameter == "response": + allotted_seconds = service_level.get("response_time") + elif parameter == "resolution": + allotted_seconds = service_level.get("resolution_time") else: frappe.throw(_("{0} parameter is invalid").format(parameter)) - allotted_hours = 0 - if time_period == 'Hour': - allotted_hours = allotted_days - allotted_days = 0 - elif time_period == 'Week': - allotted_days *= 7 - - expected_time_is_set = 1 if allotted_days == 0 and time_period in ['Day', 'Week'] else 0 + expected_time_is_set = 0 support_days = {} for service in service_level.get("support_and_resolution"): support_days[service.workday] = frappe._dict({ - 'start_time': service.start_time, - 'end_time': service.end_time, + "start_time": service.start_time, + "end_time": service.end_time, }) holidays = get_holidays(service_level.get("holiday_list")) @@ -264,25 +317,22 @@ def get_expected_time_for(parameter, service_level, start_date_time): if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \ else support_days[current_weekday].start_time end_time = support_days[current_weekday].end_time - time_left_today = time_diff_in_hours(end_time, start_time) + time_left_today = time_diff_in_seconds(end_time, start_time) # no time left for support today - if time_left_today < 0: pass - elif time_period == 'Hour': - if time_left_today >= allotted_hours: + if time_left_today <= 0: pass + elif allotted_seconds: + if time_left_today >= allotted_seconds: expected_time = datetime.combine(getdate(current_date_time), get_time(start_time)) - expected_time = add_to_date(expected_time, hours=allotted_hours) + expected_time = add_to_date(expected_time, seconds=allotted_seconds) expected_time_is_set = 1 else: - allotted_hours = allotted_hours - time_left_today - else: - allotted_days -= 1 - expected_time_is_set = allotted_days <= 0 + allotted_seconds = allotted_seconds - time_left_today if not expected_time_is_set: current_date_time = add_to_date(current_date_time, days=1) - if end_time and time_period != 'Hour': + if end_time and allotted_seconds >= 86400: current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time)) else: current_date_time = expected_time @@ -311,6 +361,36 @@ def set_service_level_agreement_variance(issue=None): if variance < 0: frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + +def set_resolution_time(issue): + # total time taken from issue creation to closing + resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) + issue.db_set("resolution_time", resolution_time) + + +def set_user_resolution_time(issue): + # total time taken by a user to close the issue apart from wait_time + communications = frappe.get_list("Communication", filters={ + "reference_doctype": issue.doctype, + "reference_name": issue.name + }, + fields=["sent_or_received", "name", "creation"], + order_by="creation" + ) + + pending_time = [] + for i in range(len(communications)): + if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": + wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if wait_time > 0: + pending_time.append(wait_time) + + total_pending_time = sum(pending_time) + resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation) + user_resolution_time = resolution_time_in_secs - total_pending_time + issue.db_set("user_resolution_time", user_resolution_time) + + def get_list_context(context=None): return { "title": _("Issues"), @@ -318,7 +398,7 @@ def get_list_context(context=None): "row_template": "templates/includes/issue_row.html", "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True + "no_breadcrumbs": True } @@ -326,12 +406,12 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord from frappe.www.list import get_list user = frappe.session.user - contact = frappe.db.get_value('Contact', {'user': user}, 'name') + contact = frappe.db.get_value("Contact", {"user": user}, "name") customer = None if contact: - contact_doc = frappe.get_doc('Contact', contact) - customer = contact_doc.get_link_for('Customer') + contact_doc = frappe.get_doc("Contact", contact) + customer = contact_doc.get_link_for("Customer") ignore_permissions = False if is_website_user(): diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 7a5e3e300db..fb8ceb53b21 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -5,15 +5,18 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues -from frappe.utils import now_datetime, get_datetime +from frappe.utils import now_datetime, get_datetime, flt import datetime from datetime import timedelta class TestIssue(unittest.TestCase): - def test_response_time_and_resolution_time_based_on_different_sla(self): + def setUp(self): + frappe.db.sql("delete from `tabService Level Agreement`") + frappe.db.sql("delete from `tabEmployee`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) create_service_level_agreements_for_issues() + def test_response_time_and_resolution_time_based_on_different_sla(self): creation = datetime.datetime(2019, 3, 4, 12, 0) # make issue with customer specific SLA @@ -72,8 +75,67 @@ class TestIssue(unittest.TestCase): self.assertEqual(issue.agreement_fulfilled, 'Fulfilled') -def make_issue(creation=None, customer=None, index=0): + def test_issue_metrics(self): + creation = datetime.datetime(2020, 3, 4, 4, 0) + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 4, 15) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + creation = datetime.datetime(2020, 3, 4, 5, 0) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 5, 5) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + issue.reload() + issue.status = 'Closed' + issue.save() + + self.assertEqual(issue.avg_response_time, 600) + self.assertEqual(issue.resolution_time, 3900) + self.assertEqual(issue.user_resolution_time, 1200) + + def test_hold_time_on_replied(self): + creation = datetime.datetime(2020, 3, 4, 4, 0) + + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 4, 15) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) + issue.reload() + issue.status = 'Replied' + issue.save() + + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + + creation = datetime.datetime(2020, 3, 4, 5, 0) + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 0) + create_communication(issue.name, "test@example.com", "Received", creation) + + issue.reload() + self.assertEqual(flt(issue.total_hold_time, 2), 2700) + self.assertEqual(issue.resolution_by, datetime.datetime(2020, 3, 4, 16, 45)) + + creation = datetime.datetime(2020, 3, 4, 5, 5) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + issue.reload() + issue.status = 'Closed' + issue.save() + + issue.reload() + self.assertEqual(flt(issue.total_hold_time, 2), 2700) + + +def make_issue(creation=None, customer=None, index=0): issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), @@ -86,6 +148,7 @@ def make_issue(creation=None, customer=None, index=0): return issue + def create_customer(name, customer_group, territory): create_customer_group(customer_group) @@ -99,6 +162,7 @@ def create_customer(name, customer_group, territory): "territory": territory }).insert(ignore_permissions=True) + def create_customer_group(customer_group): if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}): @@ -107,6 +171,7 @@ def create_customer_group(customer_group): "customer_group_name": customer_group }).insert(ignore_permissions=True) + def create_territory(territory): if not frappe.db.exists("Territory", {"territory_name": territory}): @@ -114,3 +179,21 @@ def create_territory(territory): "doctype": "Territory", "territory_name": territory, }).insert(ignore_permissions=True) + + +def create_communication(reference_name, sender, sent_or_received, creation): + issue = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": sent_or_received, + "email_status": "Open", + "subject": "Test Issue", + "sender": sender, + "content": "Test", + "status": "Linked", + "reference_doctype": "Issue", + "creation": creation, + "reference_name": reference_name + }) + issue.save() diff --git a/erpnext/support/doctype/pause_sla_on_status/__init__.py b/erpnext/support/doctype/pause_sla_on_status/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json new file mode 100644 index 00000000000..5b03f25f489 --- /dev/null +++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-06-05 13:59:43.265588", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-05 15:15:29.986608", + "modified_by": "Administrator", + "module": "Support", + "name": "Pause SLA On Status", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py new file mode 100644 index 00000000000..a3b547e4801 --- /dev/null +++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PauseSLAOnStatus(Document): + pass diff --git a/erpnext/support/doctype/service_level/service_level.js b/erpnext/support/doctype/service_level/service_level.js deleted file mode 100644 index abe254bd036..00000000000 --- a/erpnext/support/doctype/service_level/service_level.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Service Level', { - -}); diff --git a/erpnext/support/doctype/service_level/service_level.json b/erpnext/support/doctype/service_level/service_level.json deleted file mode 100644 index dced3aa9e90..00000000000 --- a/erpnext/support/doctype/service_level/service_level.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "autoname": "field:service_level", - "creation": "2018-11-19 12:44:30.407502", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "service_level", - "employee_group", - "column_break_2", - "holiday_list", - "default_priority", - "response_and_resoution_time", - "priorities", - "section_break_01", - "support_and_resolution" - ], - "fields": [ - { - "fieldname": "service_level", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Level", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "holiday_list", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Holiday List (ignored during SLA calculation)", - "options": "Holiday List", - "reqd": 1 - }, - { - "fieldname": "employee_group", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Employee Group", - "options": "Employee Group" - }, - { - "fieldname": "response_and_resoution_time", - "fieldtype": "Section Break", - "label": "Response and Resoution Time" - }, - { - "fieldname": "section_break_01", - "fieldtype": "Section Break", - "label": "Support Hours" - }, - { - "fieldname": "support_and_resolution", - "fieldtype": "Table", - "label": "Support and Resolution", - "options": "Service Day", - "reqd": 1 - }, - { - "fieldname": "priorities", - "fieldtype": "Table", - "label": "Priorities", - "options": "Service Level Priority", - "reqd": 1 - }, - { - "fieldname": "default_priority", - "fieldtype": "Link", - "label": "Default Priority", - "options": "Issue Priority", - "read_only": 1 - } - ], - "modified": "2019-06-06 12:58:03.464056", - "modified_by": "Administrator", - "module": "Support", - "name": "Service Level", - "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": "All", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/service_level.py b/erpnext/support/doctype/service_level/service_level.py deleted file mode 100644 index 89fa25c2338..00000000000 --- a/erpnext/support/doctype/service_level/service_level.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.model.document import Document -from datetime import datetime -from frappe.utils import get_weekdays - -class ServiceLevel(Document): - - def validate(self): - self.check_priorities() - self.check_support_and_resolution() - - def check_priorities(self): - default_priority = [] - priorities = [] - - for priority in self.priorities: - # Check if response and resolution time is set for every priority - if not (priority.response_time or priority.resolution_time): - frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx)) - - priorities.append(priority.priority) - - if priority.default_priority: - default_priority.append(priority.default_priority) - - if priority.response_time_period == "Hour": - response = priority.response_time * 0.0416667 - elif priority.response_time_period == "Day": - response = priority.response_time - elif priority.response_time_period == "Week": - response = priority.response_time * 7 - - if priority.resolution_time_period == "Hour": - resolution = priority.resolution_time * 0.0416667 - elif priority.resolution_time_period == "Day": - resolution = priority.resolution_time - elif priority.resolution_time_period == "Week": - resolution = priority.resolution_time * 7 - - if response > resolution: - frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) - - # Check if repeated priority - if not len(set(priorities)) == len(priorities): - repeated_priority = get_repeated(priorities) - frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority)) - - # Check if repeated default priority - if not len(set(default_priority)) == len(default_priority): - frappe.throw(_("Select only one Priority as Default.")) - - # set default priority from priorities - try: - self.default_priority = next(d.priority for d in self.priorities if d.default_priority) - except Exception: - frappe.throw(_("Select a Default Priority.")) - - def check_support_and_resolution(self): - week = get_weekdays() - support_days = [] - - for support_and_resolution in self.support_and_resolution: - # Check if start and end time is set for every support day - if not (support_and_resolution.start_time or support_and_resolution.end_time): - frappe.throw(_("Set Start Time and End Time for \ - Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx))) - - support_days.append(support_and_resolution.workday) - support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 - - if support_and_resolution.start_time >= support_and_resolution.end_time: - frappe.throw(_("Start Time can't be greater than or equal to End Time \ - for {0}.".format(support_and_resolution.workday))) - - # Check for repeated workday - if not len(set(support_days)) == len(support_days): - repeated_days = get_repeated(support_days) - frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) - -def get_repeated(values): - unique_list = [] - diff = [] - for value in values: - if value not in unique_list: - unique_list.append(str(value)) - else: - if value not in diff: - diff.append(str(value)) - return " ".join(diff) diff --git a/erpnext/support/doctype/service_level/service_level_dashboard.py b/erpnext/support/doctype/service_level/service_level_dashboard.py deleted file mode 100644 index 393095e1179..00000000000 --- a/erpnext/support/doctype/service_level/service_level_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from frappe import _ - -def get_data(): - return { - 'fieldname': 'service_level', - 'transactions': [ - { - 'label': _('Service Level Agreement'), - 'items': ['Service Level Agreement'] - } - ] - } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/test_service_level.py b/erpnext/support/doctype/service_level/test_service_level.py deleted file mode 100644 index 09577df1663..00000000000 --- a/erpnext/support/doctype/service_level/test_service_level.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals -from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group -from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities - -import frappe -import unittest - -class TestServiceLevel(unittest.TestCase): - - def test_service_level(self): - employee_group = make_employee_group() - make_holiday_list() - make_priorities() - - # Default Service Level - test_make_service_level = create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6) - get_make_service_level = get_service_level("__Test Service Level") - - self.assertEqual(test_make_service_level.name, get_make_service_level.name) - self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list) - self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group) - - # Service Level - test_make_service_level = create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3) - get_make_service_level = get_service_level("_Test Service Level") - - self.assertEqual(test_make_service_level.name, get_make_service_level.name) - self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list) - self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group) - - -def create_service_level(service_level, holiday_list, employee_group, response_time, resolution_time): - sl = frappe.get_doc({ - "doctype": "Service Level", - "service_level": service_level, - "holiday_list": holiday_list, - "employee_group": employee_group, - "priorities": [ - { - "priority": "Low", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "Medium", - "response_time": response_time, - "default_priority": 1, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "High", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - } - ], - "support_and_resolution": [ - { - "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Saturday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Sunday", - "start_time": "10:00:00", - "end_time": "18:00:00", - } - ] - }) - - sl_exists = frappe.db.exists("Service Level", {"service_level": service_level}) - - if not sl_exists: - sl.insert() - return sl - else: - return frappe.get_doc("Service Level", {"service_level": service_level}) - -def get_service_level(service_level): - return frappe.get_doc("Service Level", service_level) - -def make_holiday_list(): - holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") - if not holiday_list: - now = frappe.utils.now_datetime() - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "__Test Holiday List", - "from_date": "2019-01-01", - "to_date": "2019-12-31", - "holidays": [ - { - "description": "Test Holiday 1", - "holiday_date": "2019-03-05" - }, - { - "description": "Test Holiday 2", - "holiday_date": "2019-03-07" - }, - { - "description": "Test Holiday 3", - "holiday_date": "2019-02-11" - }, - ] - }).insert() - -def create_service_level_for_sla(): - employee_group = make_employee_group() - make_holiday_list() - make_priorities() - - # Default Service Level - create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6) - - # Service Level - create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 1d486f48346..5346195a396 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -2,28 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Service Level Agreement', { - service_level: function(frm) { - frm.fields_dict.support_and_resolution.grid.remove_all(); - frappe.call({ - "method": "frappe.client.get", - args: { - doctype: "Service Level", - name: frm.doc.service_level - }, - callback: function(data){ - let count = Math.max(data.message.priorities.length, data.message.support_and_resolution.length); - let i = 0; - while (i < count){ - if (data.message.priorities[i]) { - frm.add_child("priorities", data.message.priorities[i]); - } - if (data.message.support_and_resolution[i]) { - frm.add_child("support_and_resolution", data.message.support_and_resolution[i]); - } - i++; - } - frm.refresh(); - } + setup: function(frm) { + let allow_statuses = []; + const exclude_statuses = ['Open', 'Closed', 'Resolved']; + + frappe.model.with_doctype('Issue', () => { + let statuses = frappe.meta.get_docfield('Issue', 'status', frm.doc.name).options; + statuses = statuses.split('\n'); + allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); + frappe.meta.get_docfield('Pause SLA On Status', 'status', frm.doc.name).options = [''].concat(allow_statuses); }); - }, -}); + } +}); \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 9a83ca7ac04..939c1999828 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:SLA-{service_level}-{####}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", @@ -6,12 +7,13 @@ "engine": "InnoDB", "field_order": [ "enable", + "section_break_2", "service_level", + "default_priority", "default_service_level_agreement", - "holiday_list", "column_break_2", "employee_group", - "default_priority", + "holiday_list", "entity_section", "entity_type", "column_break_10", @@ -21,49 +23,40 @@ "active", "column_break_7", "end_date", + "section_break_18", + "pause_sla_on", "response_and_resolution_time_section", "priorities", "support_and_resolution_section_break", "support_and_resolution" ], "fields": [ - { - "default": "0", - "depends_on": "eval: !doc.customer;", - "fieldname": "default_service_level_agreement", - "fieldtype": "Check", - "label": "Default Service Level Agreement" - }, { "fieldname": "service_level", - "fieldtype": "Link", + "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, "label": "Service Level", - "options": "Service Level", "reqd": 1 }, { - "fetch_from": "service_level.holiday_list", "fieldname": "holiday_list", "fieldtype": "Link", "label": "Holiday List", "options": "Holiday List", - "read_only": 1 + "reqd": 1 }, { "fieldname": "column_break_2", "fieldtype": "Column Break" }, { - "fetch_from": "service_level.employee_group", "fieldname": "employee_group", "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, "label": "Employee Group", - "options": "Employee Group", - "read_only": 1 + "options": "Employee Group" }, { "fieldname": "agreement_details_section", @@ -103,21 +96,15 @@ "fieldname": "support_and_resolution", "fieldtype": "Table", "label": "Support and Resolution", - "options": "Service Day" + "options": "Service Day", + "reqd": 1 }, { "fieldname": "priorities", "fieldtype": "Table", "label": "Priorities", - "options": "Service Level Priority" - }, - { - "fetch_from": "service_level.default_priority", - "fieldname": "default_priority", - "fieldtype": "Link", - "label": "Default Priority", - "options": "Issue Priority", - "read_only": 1 + "options": "Service Level Priority", + "reqd": 1 }, { "default": "1", @@ -156,9 +143,38 @@ "fieldname": "enable", "fieldtype": "Check", "label": "Enable" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "default_service_level_agreement", + "fieldtype": "Check", + "label": "Default Service Level Agreement" + }, + { + "fieldname": "default_priority", + "fieldtype": "Link", + "label": "Default Priority", + "options": "Issue Priority", + "read_only": 1 + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "pause_sla_on", + "fieldtype": "Table", + "label": "Pause SLA On", + "options": "Pause SLA On Status" } ], - "modified": "2019-07-09 17:22:16.402939", + "links": [], + "modified": "2020-06-10 12:30:15.050785", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index a399c58b168..c6923157064 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,11 +6,73 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import getdate +from frappe.utils import getdate, get_weekdays class ServiceLevelAgreement(Document): def validate(self): + self.validate_doc() + self.check_priorities() + self.check_support_and_resolution() + + def check_priorities(self): + default_priority = [] + priorities = [] + + for priority in self.priorities: + # Check if response and resolution time is set for every priority + if not (priority.response_time or priority.resolution_time): + frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx)) + + priorities.append(priority.priority) + + if priority.default_priority: + default_priority.append(priority.default_priority) + + response = priority.response_time + resolution = priority.resolution_time + + if response > resolution: + frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) + + # Check if repeated priority + if not len(set(priorities)) == len(priorities): + repeated_priority = get_repeated(priorities) + frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority)) + + # Check if repeated default priority + if not len(set(default_priority)) == len(default_priority): + frappe.throw(_("Select only one Priority as Default.")) + + # set default priority from priorities + try: + self.default_priority = next(d.priority for d in self.priorities if d.default_priority) + except Exception: + frappe.throw(_("Select a Default Priority.")) + + def check_support_and_resolution(self): + week = get_weekdays() + support_days = [] + + for support_and_resolution in self.support_and_resolution: + # Check if start and end time is set for every support day + if not (support_and_resolution.start_time or support_and_resolution.end_time): + frappe.throw(_("Set Start Time and End Time for \ + Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx))) + + support_days.append(support_and_resolution.workday) + support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 + + if support_and_resolution.start_time >= support_and_resolution.end_time: + frappe.throw(_("Start Time can't be greater than or equal to End Time \ + for {0}.".format(support_and_resolution.workday))) + + # Check for repeated workday + if not len(set(support_days)) == len(support_days): + repeated_days = get_repeated(support_days) + frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) + + def validate_doc(self): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): frappe.throw(_("Service Level Agreement tracking is not enabled.")) @@ -35,9 +97,7 @@ class ServiceLevelAgreement(Document): return frappe._dict({ "priority": priority.priority, "response_time": priority.response_time, - "response_time_period": priority.response_time_period, - "resolution_time": priority.resolution_time, - "resolution_time_period": priority.resolution_time_period + "resolution_time": priority.resolution_time }) def check_agreement_status(): @@ -110,4 +170,15 @@ def get_service_level_agreement_filters(name, customer=None): return { "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])], "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)] - } \ No newline at end of file + } + +def get_repeated(values): + unique_list = [] + diff = [] + for value in values: + if value not in unique_list: + unique_list.append(str(value)) + else: + if value not in diff: + diff.append(str(value)) + return " ".join(diff) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4a741ea4e18..07ef368cbe3 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -5,19 +5,20 @@ from __future__ import unicode_literals import frappe import unittest -from erpnext.support.doctype.service_level.test_service_level import create_service_level_for_sla +from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group +from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities class TestServiceLevelAgreement(unittest.TestCase): - - def test_service_level_agreement(self): + def setUp(self): + frappe.db.sql("delete from `tabService Level Agreement`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) - create_service_level_for_sla() - + def test_service_level_agreement(self): # Default Service Level Agreement create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, - service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=4, resolution_time=6) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type=None, entity=None, response_time=14400, resolution_time=21600) + get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) @@ -28,8 +29,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Customer customer = create_customer() create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity=customer, response_time=2, resolution_time=3) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800) get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer) self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name) @@ -40,8 +41,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Customer Group customer_group = create_customer_group() create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800) get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group) self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name) @@ -52,8 +53,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Territory territory = create_territory() create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity=territory, response_time=2, resolution_time=3) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800) get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory) self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name) @@ -71,14 +72,19 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ service_level_agreement = frappe.get_doc("Service Level Agreement", filters) return service_level_agreement -def create_service_level_agreement(default_service_level_agreement, service_level, holiday_list, employee_group, +def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group, response_time, entity_type, entity, resolution_time): + employee_group = make_employee_group() + make_holiday_list() + make_priorities() + service_level_agreement = frappe.get_doc({ "doctype": "Service Level Agreement", "enable": 1, + "service_level": "__Test Service Level", "default_service_level_agreement": default_service_level_agreement, - "service_level": service_level, + "default_priority": "Medium", "holiday_list": holiday_list, "employee_group": employee_group, "entity_type": entity_type, @@ -109,6 +115,11 @@ def create_service_level_agreement(default_service_level_agreement, service_leve "resolution_time_period": "Hour", } ], + "pause_sla_on": [ + { + "status": "Replied" + } + ], "support_and_resolution": [ { "workday": "Monday", @@ -167,6 +178,7 @@ def create_service_level_agreement(default_service_level_agreement, service_leve else: return frappe.get_doc("Service Level Agreement", service_level_agreement_exists) + def create_customer(): customer = frappe.get_doc({ "doctype": "Customer", @@ -206,23 +218,41 @@ def create_territory(): return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}) def create_service_level_agreements_for_issues(): - create_service_level_for_sla() - - create_service_level_agreement(default_service_level_agreement=1, - service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=4, resolution_time=6) + create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600) create_customer() - create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3) + create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800) create_customer_group() - create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=2, resolution_time=3) + create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800) create_territory() - create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3) + create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + +def make_holiday_list(): + holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") + if not holiday_list: + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": "__Test Holiday List", + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "holidays": [ + { + "description": "Test Holiday 1", + "holiday_date": "2019-03-05" + }, + { + "description": "Test Holiday 2", + "holiday_date": "2019-03-07" + }, + { + "description": "Test Holiday 3", + "holiday_date": "2019-02-11" + }, + ] + }).insert() diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index cd87a1c1131..65d51694cc3 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-04 05:54:03.658991", "doctype": "DocType", "editable_grid": 1, @@ -9,10 +10,8 @@ "default_priority", "sb_00", "response_time", - "response_time_period", "cb_00", - "resolution_time", - "resolution_time_period" + "resolution_time" ], "fields": [ { @@ -28,16 +27,11 @@ "fieldtype": "Section Break" }, { - "columns": 1, - "fieldname": "response_time", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Response Time" - }, - { - "columns": 1, + "columns": 2, "fieldname": "resolution_time", - "fieldtype": "Int", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Resolution Time" }, @@ -45,36 +39,31 @@ "fieldname": "cb_00", "fieldtype": "Column Break" }, - { - "columns": 2, - "fieldname": "response_time_period", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Response Time Period", - "options": "Hour\nDay\nWeek" - }, - { - "columns": 2, - "fieldname": "resolution_time_period", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Resolution Time Period", - "options": "Hour\nDay\nWeek" - }, { "fieldname": "cb_01", "fieldtype": "Column Break" }, { + "columns": 1, "default": "0", "fieldname": "default_priority", "fieldtype": "Check", "in_list_view": 1, "label": "Default Priority" + }, + { + "columns": 2, + "fieldname": "response_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "in_list_view": 1, + "label": "First Response Time" } ], "istable": 1, - "modified": "2019-05-21 06:54:42.674377", + "links": [], + "modified": "2020-06-10 12:45:47.545915", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", @@ -84,4 +73,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/support/doctype/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js index 1d1069d58bb..78adca81ca5 100644 --- a/erpnext/support/doctype/support_settings/support_settings.js +++ b/erpnext/support/doctype/support_settings/support_settings.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Support Settings', { refresh: function(frm) { - + // } }); diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json index be9e0645915..5d3d3ace59d 100644 --- a/erpnext/support/doctype/support_settings/support_settings.json +++ b/erpnext/support/doctype/support_settings/support_settings.json @@ -1,4 +1,5 @@ { + "actions": "", "creation": "2017-02-17 13:07:35.686409", "doctype": "DocType", "editable_grid": 1, @@ -21,6 +22,10 @@ "post_description_key", "post_route_key", "post_route_string", + "greetings_section_section", + "greeting_title", + "column_break_19", + "greeting_subtitle", "search_apis_sb", "search_apis" ], @@ -122,13 +127,44 @@ }, { "default": "0", + "depends_on": "eval:doc.track_service_level_agreement;", "fieldname": "allow_resetting_service_level_agreement", "fieldtype": "Check", "label": "Allow Resetting Service Level Agreement" + }, + { + "default": "We're here to help", + "fieldname": "greeting_title", + "fieldtype": "Data", + "label": "Greeting Title", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "Browse help topics", + "fieldname": "greeting_subtitle", + "fieldtype": "Data", + "label": "Greeting Subtitle", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "greetings_section_section", + "fieldtype": "Section Break", + "label": "Greetings Section", + "show_days": 1, + "show_seconds": 1 } ], "issingle": 1, - "modified": "2019-07-10 22:52:39.663873", + "links": [], + "modified": "2020-06-11 13:08:38.473616", "modified_by": "Administrator", "module": "Support", "name": "Support Settings", diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 5fd901169f0..163c955c566 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -193,14 +193,17 @@ class ItemConfigure { filtered_items_count === 1 ? filtered_items[0] : ''; + // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. + const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; + const add_to_cart = `${__('Add to cart')}`; + const product_action = in_stock ? add_to_cart : `${__('Not in Stock')}`; + const item_add_to_cart = one_item ? ` `: ''; diff --git a/erpnext/templates/generators/student_admission.html b/erpnext/templates/generators/student_admission.html index ae70df8b08b..8b153448eea 100644 --- a/erpnext/templates/generators/student_admission.html +++ b/erpnext/templates/generators/student_admission.html @@ -14,12 +14,12 @@ {%- if introduction -%}
    {{ introduction }}
    -{% endif %} +{% endif %} -{%- if application_form_route -%} +{%- if doc.enable_admission_application -%}

    + href='/student-applicant'> {{ _("Apply Now") }}

    {% endif %} diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html index c91723e91e7..646210e65f1 100644 --- a/erpnext/templates/includes/cart/address_card.html +++ b/erpnext/templates/includes/cart/address_card.html @@ -3,7 +3,7 @@
    -
    {{ address.name }}
    +
    {{ address.title }}

    {{ address.display }}

    diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 60de3af17bf..aa25c885fee 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -109,7 +109,7 @@ frappe.ready(() => { reqd: 1 }, { - label: __('Pin Code'), + label: __('Postal Code'), fieldname: 'pincode', fieldtype: 'Data' }, diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html index cf7661ee3fa..4274ba12cf5 100644 --- a/erpnext/templates/includes/footer/footer_powered.html +++ b/erpnext/templates/includes/footer/footer_powered.html @@ -10,9 +10,17 @@ 'Agriculture': '/agriculture', 'Hospitality': '' } %} + {% set link = '' %} +{% set label = domains[0].domain %} {% if domains %} - {% set link = links[domains[0].domain] %} + {% set link = links[label] %} {% endif %} -Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + domains[0].domain + ' Companies') if domains else '' }} +{% if label == "Services" %} + {% set label = "Service" %} +{% endif %} + + + +Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }} diff --git a/erpnext/www/support/__init__.py b/erpnext/www/support/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/www/support/index.html b/erpnext/www/support/index.html new file mode 100644 index 00000000000..93da503dbb0 --- /dev/null +++ b/erpnext/www/support/index.html @@ -0,0 +1,58 @@ +{% extends "templates/web.html" %} + +{% block content %} +
    +
    +
    +

    {{ greeting_title or _("We're here to help!") }}

    + {% if greeting_subtitle %} +

    {{ greeting_subtitle }}

    + {% endif %} +
    +
    +
    + +{% if favorite_article_list %} +
    +
    +

    {{ _("Frequently Read Articles") }}

    +
    + {% for favorite_article in favorite_article_list %} +
    +
    +
    +
    + {{ favorite_article['category'] }}
    +

    {{ favorite_article['title'] }}

    +

    {{ favorite_article['description'] }}

    +
    + +
    +
    + {% endfor %} +
    +
    +
    +{% endif %} + +{% if help_article_list %} +
    +
    +

    {{ _("Help Articles") }}

    +
    + {% for item in help_article_list %} +
    +
    {{ item['category'].name }}
    +
    + {% for article in item['articles'] %} + {{ article.title }} + {% endfor %} +
    +
    + {% endfor %} +
    +
    +
    +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/erpnext/www/support/index.py b/erpnext/www/support/index.py new file mode 100644 index 00000000000..5d267430c16 --- /dev/null +++ b/erpnext/www/support/index.py @@ -0,0 +1,74 @@ +from __future__ import unicode_literals +import frappe + +def get_context(context): + context.no_cache = 1 + context.align_greeting = '' + setting = frappe.get_doc("Support Settings") + + context.greeting_title = setting.greeting_title + context.greeting_subtitle = setting.greeting_subtitle + + # Support content + favorite_articles = get_favorite_articles_by_page_view() + if len(favorite_articles) < 6: + name_list = [] + if favorite_articles: + for article in favorite_articles: + name_list.append(article.name) + for record in (frappe.get_all("Help Article", + fields=["title", "content", "route", "category"], + filters={"name": ['not in', tuple(name_list)], "published": 1}, + order_by="creation desc", limit=(6-len(favorite_articles)))): + favorite_articles.append(record) + + context.favorite_article_list = get_favorite_articles(favorite_articles) + context.help_article_list = get_help_article_list() + +def get_favorite_articles_by_page_view(): + return frappe.db.sql( + """ + SELECT + t1.name as name, + t1.title as title, + t1.content as content, + t1.route as route, + t1.category as category, + count(t1.route) as count + FROM `tabHelp Article` AS t1 + INNER JOIN + `tabWeb Page View` AS t2 + ON t1.route = t2.path + WHERE t1.published = 1 + GROUP BY route + ORDER BY count DESC + LIMIT 6; + """, as_dict=True) + +def get_favorite_articles(favorite_articles): + favorite_article_list=[] + for article in favorite_articles: + description = frappe.utils.strip_html(article.content) + if len(description) > 120: + description = description[:120] + '...' + favorite_article_dict = { + 'title': article.title, + 'description': description, + 'route': article.route, + 'category': article.category, + } + favorite_article_list.append(favorite_article_dict) + return favorite_article_list + +def get_help_article_list(): + help_article_list=[] + category_list = frappe.get_all("Help Category", fields="name") + for category in category_list: + help_articles = frappe.get_all("Help Article", fields="*", filters={"category": category.name, "published": 1}, order_by="modified desc", limit=5) + if help_articles: + help_aricles_per_caetgory = { + 'category': category, + 'articles': help_articles, + } + help_article_list.append(help_aricles_per_caetgory) + return help_article_list \ No newline at end of file