diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index cccced8e0be..cf1748f6a7f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -109,12 +109,13 @@ class Account(NestedSet): if not descendants: return parent_acc_name_map = {} - parent_acc_name = frappe.db.get_value('Account', self.parent_account, "account_name") + parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \ + ["account_name", "account_number"]) for d in frappe.db.get_values('Account', - {"company": ["in", descendants], "account_name": parent_acc_name}, + { "company": ["in", descendants], "account_name": parent_acc_name, + "account_number": parent_acc_number }, ["company", "name"], as_dict=True): parent_acc_name_map[d["company"]] = d["name"] - if not parent_acc_name_map: return self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index deedafdfb5d..33ae45439e7 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -15,8 +15,8 @@ def upload_bank_statement(): with open(frappe.uploaded_file, "rb") as upfile: fcontent = upfile.read() else: - from frappe.utils.file_manager import get_uploaded_content - fname, fcontent = get_uploaded_content() + fcontent = frappe.local.uploaded_file + fname = frappe.local.uploaded_filename if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')): from frappe.utils.csvutils import read_csv_content diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index acfc660c4f7..997937738b6 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -332,6 +332,7 @@ "label": "Reference" }, { + "depends_on": "eval:doc.docstatus==0", "fieldname": "get_outstanding_invoice", "fieldtype": "Button", "label": "Get Outstanding Invoice" @@ -575,7 +576,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-06 12:59:43.151721", + "modified": "2019-12-08 13:02:30.016610", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index eda59abf04e..6133b1ccd4b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -350,13 +350,13 @@ def get_amount(ref_doc): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) - if dt in ["Sales Invoice", "Purchase Invoice"]: + elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate - if dt == "Fees": + elif dt == "Fees": grand_total = ref_doc.outstanding_amount if grand_total > 0 : diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 430dce7ddbb..e871d98af6a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -34,8 +34,7 @@ class PricingRule(Document): def validate_duplicate_apply_on(self): field = apply_on_dict.get(self.apply_on) - values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field)] - + values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] if len(values) != len(set(values)): frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on)) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 637e503e658..7af6748254f 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -284,7 +284,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): status = True # if user has created item price against the transaction UOM - if rule.get("uom") == args.get("uom"): + if args and rule.get("uom") == args.get("uom"): conversion_factor = 1.0 if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3bb3df8dbd9..917acba92c9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -248,7 +248,7 @@ class PurchaseInvoice(BuyingController): def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): - if item.expense_account not in against_accounts: + if item.expense_account and (item.expense_account not in against_accounts): against_accounts.append(item.expense_account) self.against_expense_account = ",".join(against_accounts) @@ -830,7 +830,11 @@ class PurchaseInvoice(BuyingController): ) def make_gle_for_rounding_adjustment(self, gl_entries): - if self.rounding_adjustment: + # if rounding adjustment in small and conversion rate is also small then + # base_rounding_adjustment may become zero due to small precision + # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 + # then base_rounding_adjustment becomes zero and error is thrown in GL Entry + if self.rounding_adjustment and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) diff --git a/erpnext/accounts/doctype/purchase_invoice/regional/india.js b/erpnext/accounts/doctype/purchase_invoice/regional/india.js new file mode 100644 index 00000000000..81488a2c52a --- /dev/null +++ b/erpnext/accounts/doctype/purchase_invoice/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Invoice'); diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 27d8233a44b..acb0398b5c0 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -507,7 +508,8 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -523,13 +525,15 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_expense", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "fieldname": "reference", @@ -766,7 +770,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-21 16:27:52.043744", + "links": [], + "modified": "2019-12-04 12:23:17.046413", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index bc42630d474..a18fec61cf3 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -1,300 +1,108 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2013-01-10 16:34:08", - "custom": 0, - "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:08", + "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", + "doctype": "DocType", + "document_type": "Setup", + "field_order": [ + "title", + "is_default", + "disabled", + "column_break4", + "company", + "tax_category", + "section_break6", + "taxes" + ], "fields": [ { - "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": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "oldfieldname": "title", - "oldfieldtype": "Data", - "permlevel": 0, - "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": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "oldfieldname": "title", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disabled", - "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": "disabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disabled" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break4", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "section_break6", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "taxes", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldname": "purchase_tax_details", - "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges", - "permlevel": 0, - "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": "taxes", + "fieldtype": "Table", + "label": "Purchase Taxes and Charges", + "oldfieldname": "purchase_tax_details", + "oldfieldtype": "Table", + "options": "Purchase Taxes and Charges" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-money", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:18:44.095798", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Taxes and Charges Template", - "owner": "wasim@webnotestech.com", + ], + "icon": "fa fa-money", + "idx": 1, + "modified": "2019-11-25 13:05:26.220275", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Taxes and Charges Template", + "owner": "wasim@webnotestech.com", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Purchase User" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index c8305e325f6..48fa364faf0 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,3 +1,7 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Sales Invoice'); + frappe.ui.form.on("Sales Invoice", { setup: function(frm) { frm.set_query('transporter', function() { @@ -35,4 +39,5 @@ frappe.ui.form.on("Sales Invoice", { }, __("Make")); } } + }); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 2ea74f6d854..7f4ae3c1fc4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -697,8 +697,8 @@ frappe.ui.form.on('Sales Invoice', { if (frm.doc.company) { frappe.call({ - method:"frappe.contacts.doctype.address.address.get_default_address", - args:{ doctype:'Company',name:frm.doc.company}, + method:"erpnext.setup.doctype.company.company.get_default_company_address", + args:{name:frm.doc.company, existing_address: frm.doc.company_address}, callback: function(r){ if (r.message){ frm.set_value("company_address",r.message) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index def671c19b7..0f4d4451be9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -535,9 +535,7 @@ class SalesInvoice(SellingController): for i in dic: if frappe.db.get_single_value('Selling Settings', dic[i][0]) == 'Yes': for d in self.get('items'): - is_stock_item = frappe.get_cached_value('Item', d.item_code, 'is_stock_item') - if (d.item_code and is_stock_item == 1\ - and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): + if (d.item_code and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): msgprint(_("{0} is mandatory for Item {1}").format(i,d.item_code), raise_exception=1) @@ -953,7 +951,7 @@ class SalesInvoice(SellingController): ) def make_gle_for_rounding_adjustment(self, gl_entries): - if flt(self.rounding_adjustment, self.precision("rounding_adjustment")): + if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 779ac4f656c..b2294e4318f 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -484,7 +485,8 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -500,13 +502,15 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_revenue", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "collapsible": 1, @@ -783,7 +787,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-07-16 16:36:46.527606", + "links": [], + "modified": "2019-12-04 12:22:38.517710", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json index 29e15d165fa..19781bdffaa 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -1,299 +1,119 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2013-01-10 16:34:09", - "custom": 0, - "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:09", + "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "title", + "is_default", + "disabled", + "column_break_3", + "company", + "tax_category", + "section_break_5", + "taxes" + ], "fields": [ { - "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": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "oldfieldname": "title", - "oldfieldtype": "Data", - "permlevel": 0, - "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": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "oldfieldname": "title", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "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": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "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_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "oldfieldname": "company", - "oldfieldtype": "Link", - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "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_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "* Will be calculated in the transaction.", - "fieldname": "taxes", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldname": "other_charges", - "oldfieldtype": "Table", - "options": "Sales Taxes and Charges", - "permlevel": 0, - "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 + "description": "* Will be calculated in the transaction.", + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Sales Taxes and Charges", + "oldfieldname": "other_charges", + "oldfieldtype": "Table", + "options": "Sales Taxes and Charges" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-money", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:18:41.743257", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Taxes and Charges Template", - "owner": "Administrator", + ], + "icon": "fa fa-money", + "idx": 1, + "modified": "2019-11-25 13:06:03.279099", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Taxes and Charges Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 1, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 59936d5116d..156f2181b87 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -23,7 +23,7 @@ class DuplicatePartyAccountError(frappe.ValidationError): pass @frappe.whitelist() def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True, - party_address=None, shipping_address=None, pos_profile=None): + party_address=None, company_address=None, shipping_address=None, pos_profile=None): if not party: return {} @@ -31,14 +31,14 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details(party, account, party_type, company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions, - fetch_payment_terms_template, party_address, shipping_address, pos_profile) + fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile) def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, - fetch_payment_terms_template=True, party_address=None, shipping_address=None, pos_profile=None): + fetch_payment_terms_template=True, party_address=None, company_address=None,shipping_address=None, pos_profile=None): - out = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) - party = out[party_type.lower()] + party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) + party = party_details[party_type.lower()] if not ignore_permissions and not frappe.has_permission(party_type, "read", party): frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) @@ -46,76 +46,81 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party = frappe.get_doc(party_type, party) currency = party.default_currency if party.get("default_currency") else get_company_currency(company) - party_address, shipping_address = set_address_details(out, party, party_type, doctype, company, party_address, shipping_address) - set_contact_details(out, party, party_type) - set_other_values(out, party, party_type) - set_price_list(out, party, party_type, price_list, pos_profile) + party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) + set_contact_details(party_details, party, party_type) + set_other_values(party_details, party, party_type) + set_price_list(party_details, party, party_type, price_list, pos_profile) - out["tax_category"] = get_address_tax_category(party.get("tax_category"), + party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - out["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=out.customer_group, supplier_group=out.supplier_group, tax_category=out.tax_category, - billing_address=party_address, shipping_address=shipping_address) + + if not party_details.get("taxes_and_charges"): + party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - out["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) - if not out.get("currency"): - out["currency"] = currency + if not party_details.get("currency"): + party_details["currency"] = currency # sales team if party_type=="Customer": - out["sales_team"] = [{ + party_details["sales_team"] = [{ "sales_person": d.sales_person, "allocated_percentage": d.allocated_percentage or None } for d in party.get("sales_team")] # supplier tax withholding category if party_type == "Supplier" and party: - out["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") + party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") - return out + return party_details -def set_address_details(out, party, party_type, doctype=None, company=None, party_address=None, shipping_address=None): +def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None): billing_address_field = "customer_address" if party_type == "Lead" \ else party_type.lower() + "_address" - out[billing_address_field] = party_address or get_default_address(party_type, party.name) + party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) if doctype: - out.update(get_fetch_values(doctype, billing_address_field, out[billing_address_field])) + party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])) # address display - out.address_display = get_address_display(out[billing_address_field]) + party_details.address_display = get_address_display(party_details[billing_address_field]) # shipping address if party_type in ["Customer", "Lead"]: - out.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) - out.shipping_address = get_address_display(out["shipping_address_name"]) + party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) + party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) if doctype: - out.update(get_fetch_values(doctype, 'shipping_address_name', out.shipping_address_name)) + party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name)) - if doctype and doctype in ['Delivery Note', 'Sales Invoice']: - out.update(get_company_address(company)) - if out.company_address: - out.update(get_fetch_values(doctype, 'company_address', out.company_address)) - get_regional_address_details(out, doctype, company) + if company_address: + party_details.update({'company_address': company_address}) + else: + party_details.update(get_company_address(company)) - elif doctype and doctype == "Purchase Invoice": - out.update(get_company_address(company)) - if out.company_address: - out["shipping_address"] = shipping_address or out["company_address"] - out.shipping_address_display = get_address_display(out["shipping_address"]) - out.update(get_fetch_values(doctype, 'shipping_address', out.shipping_address)) - get_regional_address_details(out, doctype, company) + if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']: + if party_details.company_address: + party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address)) + get_regional_address_details(party_details, doctype, company) - return out.get(billing_address_field), out.shipping_address_name + elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: + if party_details.company_address: + party_details["shipping_address"] = shipping_address or party_details["company_address"] + party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) + party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address)) + get_regional_address_details(party_details, doctype, company) + + return party_details.get(billing_address_field), party_details.shipping_address_name @erpnext.allow_regional -def get_regional_address_details(out, doctype, company): +def get_regional_address_details(party_details, doctype, company): pass -def set_contact_details(out, party, party_type): - out.contact_person = get_default_contact(party_type, party.name) +def set_contact_details(party_details, party, party_type): + party_details.contact_person = get_default_contact(party_type, party.name) - if not out.contact_person: - out.update({ + if not party_details.contact_person: + party_details.update({ "contact_person": None, "contact_display": None, "contact_email": None, @@ -125,22 +130,22 @@ def set_contact_details(out, party, party_type): "contact_department": None }) else: - out.update(get_contact_details(out.contact_person)) + party_details.update(get_contact_details(party_details.contact_person)) -def set_other_values(out, party, party_type): +def set_other_values(party_details, party, party_type): # copy if party_type=="Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] for f in to_copy: - out[f] = party.get(f) + party_details[f] = party.get(f) # fields prepended with default in Customer doctype for f in ['currency'] \ + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []): if party.get("default_" + f): - out[f] = party.get("default_" + f) + party_details[f] = party.get("default_" + f) def get_default_price_list(party): """Return default price list for party (Document object)""" @@ -155,7 +160,7 @@ def get_default_price_list(party): return None -def set_price_list(out, party, party_type, given_price_list, pos=None): +def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list price_list = get_permitted_documents('Price List') @@ -173,9 +178,9 @@ def set_price_list(out, party, party_type, given_price_list, pos=None): price_list = get_default_price_list(party) or given_price_list if price_list: - out.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) + party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) - out["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list + party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 5f0fdc9f2c7..4a9f1b0dc44 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -88,6 +88,11 @@ frappe.query_reports["Accounts Payable Summary"] = { "label": __("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 41989bf863e..2c53f6e9971 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -60,6 +60,7 @@ class ReceivablePayableReport(object): def get_data(self): self.get_gl_entries() + self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() self.init_voucher_balance() # invoiced, paid, credit_note, outstanding @@ -103,12 +104,18 @@ class ReceivablePayableReport(object): def get_invoices(self, gle): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): - self.invoices.add(gle.voucher_no) + if self.filters.get("sales_person"): + if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ + or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) + else: + self.invoices.add(gle.voucher_no) def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) + if not row: return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) @@ -129,8 +136,13 @@ class ReceivablePayableReport(object): row.paid -= gle_balance def get_voucher_balance(self, gle): - voucher_balance = None + if self.filters.get("sales_person"): + against_voucher = gle.against_voucher or gle.voucher_no + if not (gle.party in self.sales_person_records.get("Customer", []) or \ + against_voucher in self.sales_person_records.get("Sales Invoice", [])): + return + voucher_balance = None if gle.against_voucher: # find invoice against_voucher = gle.against_voucher @@ -512,6 +524,22 @@ class ReceivablePayableReport(object): order by posting_date, party""" .format(select_fields, conditions), values, as_dict=True) + def get_sales_invoices_or_customers_based_on_sales_person(self): + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", + self.filters.get("sales_person"), ["lft", "rgt"]) + + records = frappe.db.sql(""" + select distinct parent, parenttype + from `tabSales Team` steam + where parenttype in ('Customer', 'Sales Invoice') + and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) + """, (lft, rgt), as_dict=1) + + self.sales_person_records = frappe._dict() + for d in records: + self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + def prepare_conditions(self): conditions = [""] values = [self.party_type, self.filters.report_date] @@ -564,16 +592,6 @@ class ReceivablePayableReport(object): conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") values.append(self.filters.get("sales_partner")) - if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) - - conditions.append("""exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) - and ((steam.parent = voucher_no and steam.parenttype = voucher_type) - or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) - def add_supplier_filters(self, conditions, values): if self.filters.get("supplier_group"): conditions.append("""party in (select name from tabSupplier diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 0120608a8ff..d54824b6855 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -106,6 +106,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Sales Person"), "fieldtype": "Link", "options": "Sales Person" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 8955830e09b..b607c0f7028 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -36,7 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.report_date) or {} for party, party_dict in iteritems(self.party_total): - if party_dict.outstanding <= 0: + if party_dict.outstanding == 0: continue row = frappe._dict() diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index bd2c34b3b4c..3e47906a98b 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -18,14 +18,17 @@ def execute(filters=None): return columns, data def get_data(filters, show_party_name): - party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) + if filters.get('party_type') in ('Customer', 'Supplier', 'Employee', 'Member'): + party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) if filters.get('party_type') == 'Student': party_name_field = 'first_name' elif filters.get('party_type') == 'Shareholder': party_name_field = 'title' + else: + party_name_field = 'name' party_filters = {"name": filters.get("party")} if filters.get("party") else {} - parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], + parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], filters = party_filters, order_by="name") company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") opening_balances = get_opening_balances(filters) @@ -70,7 +73,7 @@ def get_data(filters, show_party_name): # totals for col in total_row: total_row[col] += row.get(col) - + row.update({ "currency": company_currency }) @@ -78,7 +81,7 @@ def get_data(filters, show_party_name): has_value = False if (opening_debit or opening_credit or debit or credit or closing_debit or closing_credit): has_value =True - + if cint(filters.show_zero_values) or has_value: data.append(row) @@ -94,9 +97,9 @@ def get_data(filters, show_party_name): def get_opening_balances(filters): gle = frappe.db.sql(""" - select party, sum(debit) as opening_debit, sum(credit) as opening_credit + select party, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` - where company=%(company)s + where company=%(company)s and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') group by party""", { @@ -114,11 +117,11 @@ def get_opening_balances(filters): def get_balances_within_period(filters): gle = frappe.db.sql(""" - select party, sum(debit) as debit, sum(credit) as credit + select party, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` - where company=%(company)s + where company=%(company)s and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and posting_date >= %(from_date)s and posting_date <= %(to_date)s + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and ifnull(is_opening, 'No') = 'No' group by party""", { "company": filters.company, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 40f1e1efc9b..3e7f6833a0c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -517,15 +517,18 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if not is_cwip_accounting_enabled(self.asset_category): - return - assets = frappe.db.sql_list(""" select name from `tabAsset` - where ifnull(booked_fixed_asset, 0) = 0 and available_for_use_date = %s""", nowdate()) + asset_categories = frappe.db.get_all('Asset Category', fields = ['name', 'enable_cwip_accounting']) - for asset in assets: - doc = frappe.get_doc('Asset', asset) - doc.make_gl_entries() + for asset_category in asset_categories: + if cint(asset_category.enable_cwip_accounting): + assets = frappe.db.sql_list(""" select name from `tabAsset` + where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0 + and available_for_use_date = %s""", (asset_category.name, nowdate())) + + for asset in assets: + doc = frappe.get_doc('Asset', asset) + doc.make_gl_entries() def get_asset_naming_series(): meta = frappe.get_meta('Asset') @@ -607,13 +610,19 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non if asset: account = get_asset_category_account(account_name, asset=asset, asset_category = asset_category, company = company) + + if not asset and not account: + account = get_asset_category_account(account_name, asset_category = asset_category, company = company) if not account: account = frappe.get_cached_value('Company', company, account_name) if not account: - frappe.throw(_("Set {0} in asset category {1} or company {2}") - .format(account_name.replace('_', ' ').title(), asset_category, company)) + if not asset_category: + frappe.throw(_("Set {0} in company {2}").format(account_name.replace('_', ' ').title(), company)) + else: + frappe.throw(_("Set {0} in asset category {1} or company {2}") + .format(account_name.replace('_', ' ').title(), asset_category, company)) return account diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 426caaad92d..8c737d066b9 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -26,5 +26,11 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Finance Book" }, + { + fieldname:"date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today() + }, ] }; diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index f395499ad6b..57b68b4ed24 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, today, flt def execute(filters=None): filters = frappe._dict(filters or {}) @@ -86,8 +86,8 @@ def get_columns(filters): "width": 90 }, { - "label": _("Current Value"), - "fieldname": "current_value", + "label": _("Asset Value"), + "fieldname": "asset_value", "options": "Currency", "width": 90 }, @@ -114,7 +114,7 @@ def get_data(filters): data = [] conditions = get_conditions(filters) - current_value_map = get_finance_book_value_map(filters.finance_book) + depreciation_amount_map = get_finance_book_value_map(filters.date, filters.finance_book) pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() @@ -125,7 +125,9 @@ def get_data(filters): "available_for_use_date", "status", "purchase_invoice"]) for asset in assets_record: - if current_value_map.get(asset.name) is not None: + asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ + - flt(depreciation_amount_map.get(asset.name)) + if asset_value: row = { "asset_id": asset.name, "asset_name": asset.asset_name, @@ -138,19 +140,24 @@ def get_data(filters): "location": asset.location, "asset_category": asset.asset_category, "purchase_date": asset.purchase_date, - "current_value": current_value_map.get(asset.name) + "asset_value": asset_value } data.append(row) return data -def get_finance_book_value_map(finance_book=''): +def get_finance_book_value_map(date, finance_book=''): + if not date: + date = today() return frappe._dict(frappe.db.sql(''' Select - parent, value_after_depreciation - FROM `tabAsset Finance Book` + parent, SUM(depreciation_amount) + FROM `tabDepreciation Schedule` WHERE - parentfield='finance_books' - AND ifnull(finance_book, '')=%s''', cstr(finance_book))) + parentfield='schedules' + AND schedule_date<=%s + AND journal_entry IS NOT NULL + AND ifnull(finance_book, '')=%s + GROUP BY parent''', (date, cstr(finance_book)))) def get_purchase_receipt_supplier_map(): return frappe._dict(frappe.db.sql(''' Select diff --git a/erpnext/buying/doctype/purchase_order/regional/india.js b/erpnext/buying/doctype/purchase_order/regional/india.js new file mode 100644 index 00000000000..42d3995907f --- /dev/null +++ b/erpnext/buying/doctype/purchase_order/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Order'); \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index a0a1e8ed5c4..08f5d8b4d00 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.status_updater import OverAllowanceError +from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order + class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -620,6 +622,27 @@ 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. + Second Purchase Order should not add on to Blanket Orders Ordered Quantity. + """ + + bo = make_blanket_order(blanket_order_type = "Purchasing", quantity = 10, rate = 10) + + po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) + po_doc = frappe.get_doc('Purchase Order', po.get('name')) + # To test if the PO has a Blanket Order + self.assertTrue(po_doc.items[0].blanket_order) + + po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) + po_doc = frappe.get_doc('Purchase Order', po.get('name')) + # To test if the PO does NOT have a Blanket Order + self.assertEqual(po_doc.items[0].blanket_order, None) + + + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -693,7 +716,8 @@ def create_purchase_order(**args): "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1) + "include_exploded_items": args.get('include_exploded_items', 1), + "against_blanket_order": args.against_blanket_order }) if not args.do_not_save: po.insert() diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c409c1f46e0..6768dfabfea 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-24 19:29:06", "doctype": "DocType", @@ -67,6 +68,7 @@ "supplier_quotation", "supplier_quotation_item", "col_break5", + "against_blanket_order", "blanket_order", "blanket_order_rate", "item_group", @@ -511,6 +513,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", @@ -518,6 +521,7 @@ "options": "Blanket Order" }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", @@ -701,6 +705,12 @@ "label": "Manufacturer Part Number", "read_only": 1 }, + { + "default": "0", + "fieldname": "against_blanket_order", + "fieldtype": "Check", + "label": "Against Blanket Order" + }, { "default": "0", "fetch_from": "item_code.is_fixed_asset", @@ -712,7 +722,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-07 17:19:12.090355", + "links": [], + "modified": "2019-12-06 13:17:12.142799", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json similarity index 78% rename from erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json rename to erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json index 006d139eb02..d3adcb79812 100644 --- a/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json +++ b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json @@ -3,18 +3,18 @@ "app": "ERPNext", "creation": "2019-11-15 14:45:32.626641", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [ { - "label": "Supplier", + "label": "Learn More", "video_id": "zsrrVDk6VBs" } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/supplier.png", + "image_src": "/assets/erpnext/images/illustrations/supplier-onboard.png", "max_count": 3, - "modified": "2019-11-26 18:26:25.498325", + "modified": "2019-12-03 22:53:50.552445", "modified_by": "Administrator", "name": "Add A Few Suppliers", "owner": "Administrator", @@ -44,6 +44,5 @@ ], "slide_order": 50, "slide_title": "Add A Few Suppliers", - "slide_type": "Create", - "submit_method": "" + "slide_type": "Create" } \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1f8b6635958..75564afe59e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -61,7 +61,6 @@ class AccountsController(TransactionBase): _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1) def validate(self): - if not self.get('is_return'): self.validate_qty_is_not_zero() @@ -100,11 +99,23 @@ class AccountsController(TransactionBase): if self.is_return: self.validate_qty() + else: + self.validate_deferred_start_and_end_date() validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + def validate_deferred_start_and_end_date(self): + for d in self.items: + if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): + if not (d.service_start_date and d.service_end_date): + frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx)) + elif getdate(d.service_start_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) + elif getdate(self.posting_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) + def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() self.set_due_date() @@ -415,9 +426,10 @@ class AccountsController(TransactionBase): return gl_dict def validate_qty_is_not_zero(self): - for item in self.items: - if not item.qty: - frappe.throw(_("Item quantity can not be zero")) + if self.doctype != "Purchase Receipt": + for item in self.items: + if not item.qty: + frappe.throw(_("Item quantity can not be zero")) def validate_account_currency(self, account, account_currency=None): valid_currency = [self.company_currency] diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 2affba2ac40..b6c4c4707de 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -171,7 +171,7 @@ class Appointment(Document): self.save(ignore_permissions=True) def _get_verify_url(self): - verify_route = '/book-appointment/verify' + verify_route = '/book_appointment/verify' params = { 'email': self.customer_email, 'appointment': self.name diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 3050d05a7c8..00a4bd1a322 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -41,7 +41,8 @@ class EmailCampaign(Document): email_campaign_exists = frappe.db.exists("Email Campaign", { "campaign_name": self.campaign_name, "recipient": self.recipient, - "status": ("in", ["In Progress", "Scheduled"]) + "status": ("in", ["In Progress", "Scheduled"]), + "name": ("!=", self.name) }) if email_campaign_exists: frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) @@ -78,7 +79,7 @@ def send_mail(entry, email_campaign): comm = make( doctype = "Email Campaign", name = email_campaign.name, - subject = email_template.get("subject"), + subject = frappe.render_template(email_template.get("subject"), context), content = frappe.render_template(email_template.get("response"), context), sender = sender, recipients = recipient, diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 99486fa2066..2880c8050e0 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -130,10 +130,10 @@ class Opportunity(TransactionBase): def has_lost_quotation(self): lost_quotation = frappe.db.sql(""" - select q.name - from `tabQuotation` q, `tabQuotation Item` qi - where q.name = qi.parent and q.docstatus=1 - and qi.prevdoc_docname =%s and q.status = 'Lost' + select name + from `tabQuotation` + where docstatus=1 + and opportunity =%s and status = 'Lost' """, self.name) if lost_quotation: if self.has_active_quotation(): diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 9af5e22913a..8e4b4e16f9a 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils import getdate,today from frappe import _ from frappe.desk.form.linked_with import get_linked_doctypes from erpnext.education.utils import check_content_completion, check_quiz_completion class Student(Document): def validate(self): self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + self.validate_dates() if self.student_applicant: self.check_unique() @@ -19,6 +21,10 @@ class Student(Document): if frappe.get_value("Student", self.name, "title") != self.title: self.update_student_name_in_linked_doctype() + def validate_dates(self): + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): + frappe.throw(_("Date of Birth cannot be greater than today.")) + def update_student_name_in_linked_doctype(self): linked_doctypes = get_linked_doctypes("Student") for d in linked_doctypes: @@ -40,7 +46,7 @@ class Student(Document): frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) def after_insert(self): - if not frappe.get_single('Education Settings').user_creation_skip: + if not frappe.get_single('Education Settings').get('user_creation_skip'): self.create_student_user() def create_student_user(self): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ed5bf9b5d82..2a5e6d8f49a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -246,10 +246,10 @@ doc_events = { "on_trash": "erpnext.regional.check_deletion_permission" }, 'Address': { - 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code'] + 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category'] }, - ('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): { - 'validate': 'erpnext.regional.india.utils.set_place_of_supply' + ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { + 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index bc4a1b4034b..7a9727f18c4 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_days, getdate +from frappe.utils import date_diff, add_days, getdate, cint from frappe.model.document import Document -from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, get_holidays_for_employee +from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ + get_holidays_for_employee, create_additional_leave_ledger_entry class CompensatoryLeaveRequest(Document): @@ -25,16 +26,14 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("Leave Type is madatory")) def validate_attendance(self): - query = """select attendance_date, status - from `tabAttendance` where - attendance_date between %(work_from_date)s and %(work_end_date)s - and docstatus=1 and status = 'Present' and employee=%(employee)s""" + attendance = frappe.get_all('Attendance', + filters={ + 'attendance_date': ['between', (self.work_from_date, self.work_end_date)], + 'status': 'Present', + 'docstatus': 1, + 'employee': self.employee + }, fields=['attendance_date', 'status']) - attendance = frappe.db.sql(query, { - "work_from_date": self.work_from_date, - "work_end_date": self.work_end_date, - "employee": self.employee - }, as_dict=True) if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1: frappe.throw(_("You are not present all day(s) between compensatory leave request days")) @@ -50,13 +49,19 @@ class CompensatoryLeaveRequest(Document): date_difference -= 0.5 leave_period = get_leave_period(self.work_from_date, self.work_end_date, company) if leave_period: - leave_allocation = self.exists_allocation_for_period(leave_period) + leave_allocation = self.get_existing_allocation_for_period(leave_period) if leave_allocation: leave_allocation.new_leaves_allocated += date_difference - leave_allocation.submit() + leave_allocation.validate() + leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated) + leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) + + # generate additional ledger entry for the new compensatory leaves off + create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1)) + else: leave_allocation = self.create_leave_allocation(leave_period, date_difference) - self.db_set("leave_allocation", leave_allocation.name) + self.leave_allocation=leave_allocation.name else: frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) @@ -68,11 +73,16 @@ class CompensatoryLeaveRequest(Document): leave_allocation = frappe.get_doc("Leave Allocation", self.leave_allocation) if leave_allocation: leave_allocation.new_leaves_allocated -= date_difference - if leave_allocation.total_leaves_allocated - date_difference <= 0: - leave_allocation.total_leaves_allocated = 0 - leave_allocation.submit() + if leave_allocation.new_leaves_allocated - date_difference <= 0: + leave_allocation.new_leaves_allocated = 0 + leave_allocation.validate() + leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated) + leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) - def exists_allocation_for_period(self, leave_period): + # create reverse entry on cancelation + create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1)) + + def get_existing_allocation_for_period(self, leave_period): leave_allocation = frappe.db.sql(""" select name from `tabLeave Allocation` @@ -95,17 +105,18 @@ class CompensatoryLeaveRequest(Document): def create_leave_allocation(self, leave_period, date_difference): is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") - allocation = frappe.new_doc("Leave Allocation") - allocation.employee = self.employee - allocation.employee_name = self.employee_name - allocation.leave_type = self.leave_type - allocation.from_date = add_days(self.work_end_date, 1) - allocation.to_date = leave_period[0].to_date - allocation.new_leaves_allocated = date_difference - allocation.total_leaves_allocated = date_difference - allocation.description = self.reason - if is_carry_forward == 1: - allocation.carry_forward = True - allocation.save(ignore_permissions = True) + allocation = frappe.get_doc(dict( + doctype="Leave Allocation", + employee=self.employee, + employee_name=self.employee_name, + leave_type=self.leave_type, + from_date=add_days(self.work_end_date, 1), + to_date=leave_period[0].to_date, + carry_forward=cint(is_carry_forward), + new_leaves_allocated=date_difference, + total_leaves_allocated=date_difference, + description=self.reason + )) + allocation.insert(ignore_permissions=True) allocation.submit() - return allocation + return allocation \ No newline at end of file diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index f2ca1f4f5f0..1615ab30f1d 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -5,37 +5,128 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import today, add_months, add_days +from erpnext.hr.doctype.attendance_request.test_attendance_request import get_employee +from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period +from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on -# class TestCompensatoryLeaveRequest(unittest.TestCase): -# def get_compensatory_leave_request(self): -# return frappe.get_doc('Compensatory Leave Request', dict( -# employee = employee, -# work_from_date = today, -# work_to_date = today, -# reason = 'test' -# )).insert() -# -# def test_creation_of_leave_allocation(self): -# employee = get_employee() -# today = get_today() -# -# compensatory_leave_request = self.get_compensatory_leave_request(today) -# -# before = get_leave_balance(employee, compensatory_leave_request.leave_type) -# -# compensatory_leave_request.submit() -# -# self.assertEqual(get_leave_balance(employee, compensatory_leave_request.leave_type), before + 1) -# -# def test_max_compensatory_leave(self): -# employee = get_employee() -# today = get_today() -# -# compensatory_leave_request = self.get_compensatory_leave_request() -# -# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 0) -# -# self.assertRaises(MaxLeavesLimitCrossed, compensatory_leave_request.submit) -# -# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 10) -# +class TestCompensatoryLeaveRequest(unittest.TestCase): + def setUp(self): + frappe.db.sql(''' delete from `tabCompensatory Leave Request`''') + frappe.db.sql(''' delete from `tabLeave Ledger Entry`''') + frappe.db.sql(''' delete from `tabLeave Allocation`''') + frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec + create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company") + create_holiday_list() + + employee = get_employee() + employee.holiday_list = "_Test Compensatory Leave" + employee.save() + + def test_leave_balance_on_submit(self): + ''' check creation of leave allocation on submission of compensatory leave request ''' + employee = get_employee() + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + + before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today()) + compensatory_leave_request.submit() + + self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1) + + def test_leave_allocation_update_on_submit(self): + employee = get_employee() + mark_attendance(employee, date=add_days(today(), -1)) + compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1)) + compensatory_leave_request.submit() + + # leave allocation creation on submit + leaves_allocated = frappe.db.get_value('Leave Allocation', { + 'name': compensatory_leave_request.leave_allocation + }, ['total_leaves_allocated']) + self.assertEqual(leaves_allocated, 1) + + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + compensatory_leave_request.submit() + + # leave allocation updates on submission of second compensatory leave request + leaves_allocated = frappe.db.get_value('Leave Allocation', { + 'name': compensatory_leave_request.leave_allocation + }, ['total_leaves_allocated']) + self.assertEqual(leaves_allocated, 2) + + def test_creation_of_leave_ledger_entry_on_submit(self): + ''' check creation of leave ledger entry on submission of leave request ''' + employee = get_employee() + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + compensatory_leave_request.submit() + + filters = dict(transaction_name=compensatory_leave_request.leave_allocation) + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters) + + self.assertEquals(len(leave_ledger_entry), 1) + self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, 1) + + # check reverse leave ledger entry on cancellation + compensatory_leave_request.cancel() + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc') + + self.assertEquals(len(leave_ledger_entry), 2) + self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, -1) + +def get_compensatory_leave_request(employee, leave_date=today()): + prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request', + dict(leave_type='Compensatory Off', + work_from_date=leave_date, + work_end_date=leave_date, + employee=employee), 'name') + if prev_comp_leave_req: + return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req) + + return frappe.get_doc(dict( + doctype='Compensatory Leave Request', + employee=employee, + leave_type='Compensatory Off', + work_from_date=leave_date, + work_end_date=leave_date, + reason='test' + )).insert() + +def mark_attendance(employee, date=today(), status='Present'): + if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')): + attendance = frappe.get_doc({ + "doctype": "Attendance", + "employee": employee.name, + "attendance_date": date, + "status": status + }) + attendance.save() + attendance.submit() + +def create_holiday_list(): + if frappe.db.exists("Holiday List", "_Test Compensatory Leave"): + return + + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "from_date": add_months(today(), -3), + "to_date": add_months(today(), 3), + "holidays": [ + { + "description": "Test Holiday", + "holiday_date": today() + }, + { + "description": "Test Holiday 1", + "holiday_date": add_days(today(), -1) + } + ], + "holiday_list_name": "_Test Compensatory Leave" + }) + holiday_list.save() \ No newline at end of file diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 703ec06f83b..242531bd177 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime +from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -152,8 +152,8 @@ class Employee(NestedSet): elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): throw(_("Date Of Retirement must be greater than Date of Joining")) - elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) <= getdate(self.date_of_joining)): - throw(_("Relieving Date must be greater than Date of Joining")) + elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)): + throw(_("Relieving Date must be greater than or equal to Date of Joining")) elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): throw(_("Contract End Date must be greater than Date of Joining")) @@ -218,8 +218,8 @@ class Employee(NestedSet): def reset_employee_emails_cache(self): prev_doc = self.get_doc_before_save() or {} - cell_number = self.get('cell_number') - prev_number = prev_doc.get('cell_number') + cell_number = cstr(self.get('cell_number')) + prev_number = cstr(prev_doc.get('cell_number')) if (cell_number != prev_number or self.get('user_id') != prev_doc.get('user_id')): frappe.cache().hdel('employees_with_number', cell_number) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 0d37c10e9cc..570f2ef4c77 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -42,12 +42,6 @@ cur_frm.cscript.onload = function(doc) { cur_frm.set_value("posting_date", frappe.datetime.get_today()); cur_frm.cscript.clear_sanctioned(doc); } - - cur_frm.fields_dict.employee.get_query = function() { - return { - query: "erpnext.controllers.queries.employee_query" - }; - }; }; cur_frm.cscript.clear_sanctioned = function(doc) { @@ -119,7 +113,7 @@ cur_frm.cscript.calculate_total_amount = function(doc,cdt,cdn){ }; erpnext.expense_claim = { - set_title :function(frm) { + set_title: function(frm) { if (!frm.doc.task) { frm.set_value("title", frm.doc.employee_name); } @@ -131,20 +125,20 @@ erpnext.expense_claim = { frappe.ui.form.on("Expense Claim", { setup: function(frm) { - frm.trigger("set_query_for_cost_center"); - frm.trigger("set_query_for_payable_account"); frm.add_fetch("company", "cost_center", "cost_center"); frm.add_fetch("company", "default_expense_claim_payable_account", "payable_account"); - frm.set_query("employee_advance", "advances", function(doc) { + + frm.set_query("employee_advance", "advances", function() { return { filters: [ ['docstatus', '=', 1], - ['employee', '=', doc.employee], + ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], ['paid_amount', '>', 'claimed_amount'] ] }; }); + frm.set_query("expense_approver", function() { return { query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers", @@ -154,14 +148,49 @@ frappe.ui.form.on("Expense Claim", { } }; }); - frm.set_query("account_head", "taxes", function(doc) { + + frm.set_query("account_head", "taxes", function() { return { filters: [ - ['company', '=', doc.company], + ['company', '=', frm.doc.company], ['account_type', 'in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation"]] ] }; }); + + frm.set_query("cost_center", "expenses", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("payable_account", function() { + return { + filters: { + "report_type": "Balance Sheet", + "account_type": "Payable", + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("task", function() { + return { + filters: { + 'project': frm.doc.project + } + }; + }); + + frm.set_query("employee", function() { + return { + query: "erpnext.controllers.queries.employee_query" + }; + }); }, onload: function(frm) { @@ -244,30 +273,6 @@ frappe.ui.form.on("Expense Claim", { }); }, - set_query_for_cost_center: function(frm) { - frm.fields_dict["cost_center"].get_query = function() { - return { - filters: { - "company": frm.doc.company, - "is_group": 0 - } - }; - }; - }, - - set_query_for_payable_account: function(frm) { - frm.fields_dict["payable_account"].get_query = function() { - return { - filters: { - "report_type": "Balance Sheet", - "account_type": "Payable", - "company": frm.doc.company, - "is_group": 0 - } - }; - }; - }, - is_paid: function(frm) { frm.trigger("toggle_fields"); }, @@ -329,6 +334,10 @@ 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"]); + }, amount: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; var doc = frm.doc; @@ -341,6 +350,9 @@ frappe.ui.form.on("Expense Claim Detail", { cur_frm.cscript.calculate_total(doc,cdt,cdn); frm.trigger("get_taxes"); frm.trigger("calculate_grand_total"); + }, + cost_center: function(frm, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "expenses", "cost_center"); } }); @@ -411,12 +423,4 @@ frappe.ui.form.on("Expense Taxes and Charges", { tax_amount: function(frm, cdt, cdn) { frm.trigger("calculate_total_tax", cdt, cdn); } -}); - -cur_frm.fields_dict['task'].get_query = function(doc) { - return { - filters:{ - 'project': doc.project - } - }; -}; +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 5c2f4901713..b5b6823e1ce 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -43,7 +43,6 @@ "accounting_dimensions_section", "project", "dimension_col_break", - "cost_center", "more_details", "status", "amended_from", @@ -366,7 +365,7 @@ "icon": "fa fa-money", "idx": 1, "is_submittable": 1, - "modified": "2019-11-08 14:13:08.964547", + "modified": "2019-11-09 14:13:08.964547", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 59391505fa0..dfb0bb96d82 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -127,7 +127,7 @@ class ExpenseClaim(AccountsController): "debit": data.sanctioned_amount, "debit_in_account_currency": data.sanctioned_amount, "against": self.employee, - "cost_center": self.cost_center + "cost_center": data.cost_center }) ) @@ -190,8 +190,9 @@ class ExpenseClaim(AccountsController): ) def validate_account_details(self): - if not self.cost_center: - frappe.throw(_("Cost center is required to book an expense claim")) + for data in self.expenses: + if not data.cost_center: + frappe.throw(_("Cost center is required to book an expense claim")) if self.is_paid: if not self.mode_of_payment: diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index b559dfd81d1..6e97f0513d6 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -126,7 +126,7 @@ def generate_taxes(): def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): employee = frappe.db.get_value("Employee", {"status": "Active"}) - currency = frappe.db.get_value('Company', company, 'default_currency') + currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) expense_claim = { "doctype": "Expense Claim", "employee": employee, @@ -134,12 +134,15 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco "approval_status": "Approved", "company": company, 'currency': currency, - "expenses": - [{"expense_type": "Travel", + "expenses": [{ + "expense_type": "Travel", "default_account": account, - 'currency': currency, + "currency": currency, "amount": amount, - "sanctioned_amount": sanctioned_amount}]} + "sanctioned_amount": sanctioned_amount, + "cost_center": cost_center + }] + } if taxes: expense_claim.update(taxes) diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index b23fb6af0fe..b60db2c3af0 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -1,378 +1,118 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, "creation": "2013-02-22 01:27:46", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "expense_date", + "column_break_2", + "expense_type", + "default_account", + "section_break_4", + "description", + "section_break_6", + "amount", + "column_break_8", + "sanctioned_amount", + "cost_center" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expense_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Expense Date", - "length": 0, - "no_copy": 0, "oldfieldname": "expense_date", "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expense_type", "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": "Expense Claim Type", - "length": 0, - "no_copy": 0, "oldfieldname": "expense_type", "oldfieldtype": "Link", "options": "Expense Claim Type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "expense_type", "fieldname": "default_account", "fieldtype": "Link", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Default Account", - "length": 0, - "no_copy": 0, "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "", "fieldname": "description", "fieldtype": "Text Editor", - "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": "Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Small Text", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "300px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "300px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amount", "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": "Amount", - "length": 0, - "no_copy": 0, "oldfieldname": "claim_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sanctioned_amount", "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": "Sanctioned Amount", - "length": 0, "no_copy": 1, "oldfieldname": "sanctioned_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-06-10 08:41:36.122565", - "modified_by": "Administrator", + "modified": "2019-11-22 11:57:25.110942", + "modified_by": "jangeles@bai.ph", "module": "HR", "name": "Expense Claim Detail", "owner": "harshada@webnotestech.com", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js index fda6809e6b2..c4877976775 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Expense Claim Type", { return{ filters: { "is_group": 0, - "root_type": "Expense", + "root_type": frm.doc.deferred_expense_account ? "Asset" : "Expense", 'company': d.company } } diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json index d0c41221cc8..e45f640c078 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json @@ -1,181 +1,72 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:expense_type", - "beta": 0, - "creation": "2012-03-27 14:35:55", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:expense_type", + "creation": "2012-03-27 14:35:55", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "deferred_expense_account", + "expense_type", + "description", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_type", - "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": "Expense Claim Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "expense_type", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "expense_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Expense Claim Type", + "oldfieldname": "expense_type", + "oldfieldtype": "Data", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Small Text", - "permlevel": 0, - "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": "description", + "fieldtype": "Small Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "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": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Expense Claim Account", - "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": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Expense Claim Account" + }, + { + "default": "0", + "fieldname": "deferred_expense_account", + "fieldtype": "Check", + "label": "Deferred Expense Account" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 14:13:43.770829", - "modified_by": "Administrator", - "module": "HR", - "name": "Expense Claim Type", - "owner": "harshada@webnotestech.com", + ], + "icon": "fa fa-flag", + "idx": 1, + "modified": "2019-11-22 12:00:18.710408", + "modified_by": "jangeles@bai.ph", + "module": "HR", + "name": "Expense Claim Type", + "owner": "harshada@webnotestech.com", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Employee" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 874ae7a1bc2..d13bb4577cd 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -69,10 +69,14 @@ class LeaveAllocation(Document): def validate_allocation_overlap(self): leave_allocation = frappe.db.sql(""" - select name from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 - and to_date >= %s and from_date <= %s""", - (self.employee, self.leave_type, self.from_date, self.to_date)) + SELECT + name + FROM `tabLeave Allocation` + WHERE + employee=%s AND leave_type=%s + AND name <> %s AND docstatus=1 + AND to_date >= %s AND from_date <= %s""", + (self.employee, self.leave_type, self.name, self.from_date, self.to_date)) if leave_allocation: frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index db3819eff2d..14ffa0ed1f0 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -60,6 +60,7 @@ frappe.ui.form.on("Leave Application", { } } }); + $("div").remove(".form-dashboard-section.custom"); frm.dashboard.add_section( frappe.render_template('leave_application_dashboard', { data: leave_details @@ -170,7 +171,7 @@ frappe.ui.form.on("Leave Application", { frm.set_value('to_date', ''); return; } - // server call is done to include holidays in leave days calculations + // server call is done to include holidays in leave days calculations return frappe.call({ method: 'erpnext.hr.doctype.leave_application.leave_application.get_number_of_leave_days', args: { @@ -193,7 +194,7 @@ frappe.ui.form.on("Leave Application", { set_leave_approver: function(frm) { if(frm.doc.employee) { - // server call is done to include holidays in leave days calculations + // server call is done to include holidays in leave days calculations return frappe.call({ method: 'erpnext.hr.doctype.leave_application.leave_application.get_leave_approver', args: { diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0e6630541c4..915cea149d8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -351,7 +351,7 @@ class LeaveApplication(Document): pass def create_leave_ledger_entry(self, submit=True): - if self.status != 'Approved': + if self.status != 'Approved' and submit: return expiry_date = get_allocation_expiry(self.employee, self.leave_type, @@ -549,10 +549,10 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): leave_days += leave_entry.leaves elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ - and not skip_expiry_leaves(leave_entry, to_date): + and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date): leave_days += leave_entry.leaves - else: + elif leave_entry.transaction_type == 'Leave Application': if leave_entry.from_date < getdate(from_date): leave_entry.from_date = from_date if leave_entry.to_date > getdate(to_date): @@ -579,14 +579,15 @@ def skip_expiry_leaves(leave_entry, date): def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date ''' return frappe.db.sql(""" - select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward, transaction_name - from `tabLeave Ledger Entry` - where employee=%(employee)s and leave_type=%(leave_type)s - and docstatus=1 - and leaves<0 - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) + SELECT + employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, + is_carry_forward, is_expired + FROM `tabLeave Ledger Entry` + WHERE employee=%(employee)s AND leave_type=%(leave_type)s + AND docstatus=1 AND leaves<0 + AND (from_date between %(from_date)s AND %(to_date)s + OR to_date between %(from_date)s AND %(to_date)s + OR (from_date < %(from_date)s AND to_date > %(to_date)s)) """, { "from_date": from_date, "to_date": to_date, @@ -773,4 +774,4 @@ def get_leave_approver(employee): leave_approver = frappe.db.get_value('Department Approver', {'parent': department, 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') - return leave_approver + return leave_approver \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 38ae808f27e..b9c02101f1b 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -301,7 +301,7 @@ class TestLeaveApplication(unittest.TestCase): to_date = add_days(date, 2), company = "_Test Company", docstatus = 1, - status = "Approved" + status = "Approved" )) leave_application.submit() @@ -314,7 +314,7 @@ class TestLeaveApplication(unittest.TestCase): to_date = add_days(date, 8), company = "_Test Company", docstatus = 1, - status = "Approved" + status = "Approved" )) self.assertRaises(frappe.ValidationError, leave_application.insert) diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 850a08dd536..1762cf917a2 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -43,10 +43,18 @@ class TestLeavePeriod(unittest.TestCase): leave_period.grant_leave_allocation(employee=employee_doc_name) self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) -def create_leave_period(from_date, to_date): +def create_leave_period(from_date, to_date, company=None): + leave_period = frappe.db.get_value('Leave Period', + dict(company=company or erpnext.get_default_company(), + from_date=from_date, + to_date=to_date, + is_active=1), 'name') + if leave_period: + return frappe.get_doc("Leave Period", leave_period) + leave_period = frappe.get_doc({ "doctype": "Leave Period", - "company": erpnext.get_default_company(), + "company": company or erpnext.get_default_company(), "from_date": from_date, "to_date": to_date, "is_active": 1 diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.js b/erpnext/hr/doctype/payroll_entry/payroll_entry.js index adc06712ef0..d25eb6d7810 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.js @@ -31,7 +31,11 @@ frappe.ui.form.on('Payroll Entry', { } if ((frm.doc.employees || []).length) { frm.page.set_primary_action(__('Create Salary Slips'), () => { - frm.save('Submit'); + frm.save('Submit').then(()=>{ + frm.page.clear_primary_action(); + frm.refresh(); + frm.events.refresh(frm); + }); }); } } diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 1464a779368..c3e8d27557d 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -321,11 +321,11 @@ def allocate_earned_leaves(): if new_allocation == allocation.total_leaves_allocated: continue allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_earned_leave_ledger_entry(allocation, earned_leaves, today) + create_additional_leave_ledger_entry(allocation, earned_leaves, today) -def create_earned_leave_ledger_entry(allocation, earned_leaves, date): - ''' Create leave ledger entry based on the earned leave frequency ''' - allocation.new_leaves_allocated = earned_leaves +def create_additional_leave_ledger_entry(allocation, leaves, date): + ''' Create leave ledger entry for leave types ''' + allocation.new_leaves_allocated = leaves allocation.from_date = date allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() @@ -389,6 +389,7 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): def get_holidays_for_employee(employee, start_date, end_date): holiday_list = get_holiday_list_for_employee(employee) + holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%(holiday_list)s diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index faed707d600..38118bd78d4 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -44,6 +44,8 @@ def make_sales_order(source_name): target.item_name = item.get("item_name") target.description = item.get("description") target.uom = item.get("stock_uom") + target.against_blanket_order = 1 + target.blanket_order = source_name target_doc = get_mapped_doc("Blanket Order", source_name, { "Blanket Order": { @@ -71,6 +73,8 @@ def make_purchase_order(source_name): target.description = item.get("description") target.uom = item.get("stock_uom") target.warehouse = item.get("default_warehouse") + target.against_blanket_order = 1 + target.blanket_order = source_name target_doc = get_mapped_doc("Blanket Order", source_name, { "Blanket Order": { diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 8283fd7e6fc..3acaee4ffbd 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -6,7 +6,6 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { setup: function(frm) { frm.custom_make_buttons = { - 'BOM': 'Duplicate BOM', 'Work Order': 'Work Order', 'Quality Inspection': 'Quality Inspection' }; @@ -91,10 +90,6 @@ frappe.ui.form.on("BOM", { } if(frm.doc.docstatus!=0) { - frm.add_custom_button(__("Duplicate BOM"), function() { - frm.copy_doc(); - }, __("Create")); - frm.add_custom_button(__("Work Order"), function() { frm.trigger("make_work_order"); }, __("Create")); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 55799544982..e3ece569641 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -606,6 +606,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item.image, bom.project, item.stock_uom, + item.item_group, item.allow_alternative_item, item_default.default_warehouse, item_default.expense_account as expense_account, diff --git a/erpnext/manufacturing/doctype/bom/bom_dashboard.py b/erpnext/manufacturing/doctype/bom/bom_dashboard.py index 060cd53ef19..361826e2d0c 100644 --- a/erpnext/manufacturing/doctype/bom/bom_dashboard.py +++ b/erpnext/manufacturing/doctype/bom/bom_dashboard.py @@ -25,5 +25,5 @@ def get_data(): } ], 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt", - "Purchase Invoice", "Job Card", "Stock Entry"] + "Purchase Invoice", "Job Card", "Stock Entry", "BOM"] } diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 95549d5a248..bc8c22998ae 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -3,6 +3,9 @@ frappe.ui.form.on('Job Card', { refresh: function(frm) { + frappe.flags.pause_job = 0; + frappe.flags.resume_job = 0; + if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) { if (frm.doc.for_quantity != frm.doc.transferred_qty) { frm.add_custom_button(__("Material Request"), () => { @@ -13,44 +16,99 @@ frappe.ui.form.on('Job Card', { if (frm.doc.for_quantity != frm.doc.transferred_qty) { frm.add_custom_button(__("Material Transfer"), () => { frm.trigger("make_stock_entry"); - }); + }).addClass("btn-primary"); } } - if (frm.doc.docstatus == 0) { - frm.trigger("make_dashboard"); + if (frm.doc.docstatus == 0 && frm.doc.for_quantity > frm.doc.total_completed_qty + && (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + frm.trigger("prepare_timer_buttons"); + } + }, - if (!frm.doc.job_started) { - frm.add_custom_button(__("Start Job"), () => { - let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); - row.from_time = frappe.datetime.now_datetime(); - frm.set_value('job_started', 1); - frm.set_value('started_time' , row.from_time); - frm.save(); - }); - } else { - frm.add_custom_button(__("Complete Job"), () => { - let completed_time = frappe.datetime.now_datetime(); - frm.doc.time_logs.forEach(d => { - if (d.from_time && !d.to_time) { - d.to_time = completed_time; - frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.save(); + prepare_timer_buttons: function(frm) { + frm.trigger("make_dashboard"); + if (!frm.doc.job_started) { + frm.add_custom_button(__("Start"), () => { + if (!frm.doc.employee) { + frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", + fieldname: 'employee'}, d => { + if (d.employee) { + frm.set_value("employee", d.employee); } - }) - }); - } + + frm.events.start_job(frm); + }, __("Enter Value"), __("Start")); + } else { + frm.events.start_job(frm); + } + }).addClass("btn-primary"); + } else if (frm.doc.status == "On Hold") { + frm.add_custom_button(__("Resume"), () => { + frappe.flags.resume_job = 1; + frm.events.start_job(frm); + }).addClass("btn-primary"); + } else { + frm.add_custom_button(__("Pause"), () => { + frappe.flags.pause_job = 1; + frm.set_value("status", "On Hold"); + frm.events.complete_job(frm); + }); + + frm.add_custom_button(__("Complete"), () => { + let completed_time = frappe.datetime.now_datetime(); + frm.trigger("hide_timer"); + + frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), + fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { + frm.events.complete_job(frm, completed_time, data.qty); + }, __("Enter Value"), __("Complete")); + }).addClass("btn-primary"); } }, + start_job: function(frm) { + let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); + row.from_time = frappe.datetime.now_datetime(); + frm.set_value('job_started', 1); + frm.set_value('started_time' , row.from_time); + frm.set_value("status", "Work In Progress"); + + if (!frappe.flags.resume_job) { + frm.set_value('current_time' , 0); + } + + frm.save(); + }, + + complete_job: function(frm, completed_time, completed_qty) { + frm.doc.time_logs.forEach(d => { + if (d.from_time && !d.to_time) { + d.to_time = completed_time || frappe.datetime.now_datetime(); + d.completed_qty = completed_qty || 0; + + if(frappe.flags.pause_job) { + let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; + frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0)); + } else { + frm.set_value('started_time' , ''); + frm.set_value('job_started', 0); + frm.set_value('current_time' , 0); + } + + frm.save(); + } + }); + }, + make_dashboard: function(frm) { if(frm.doc.__islocal) return; frm.dashboard.refresh(); const timer = ` -
|
-
+ {{ page_title }}{% if address_line2 %}{{ address_line2 }} {% endif -%} -{{ pincode }} {{ city }} -{% if country %}{{ country }} {% endif -%} - -{% if phone %}Tel: {{ phone }} {% endif -%} -{% if fax %}Fax: {{ fax }} {% endif -%} -{% if email_id %}E-Mail: {{ email_id }} {% endif -%} +{% if country in ["Germany", "Deutschland"] %} + {{ pincode }} {{ city }} +{% else %} + {{ pincode }} {{ city | upper }} + {{ country | upper }} +{% endif %} diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 46c874b2524..0ed98b74eef 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from six import iteritems states = [ '', @@ -79,4 +80,6 @@ state_numbers = { "Uttar Pradesh": "09", "Uttarakhand": "05", "West Bengal": "19", -} \ No newline at end of file +} + +number_state_mapping = {v: k for k, v in iteritems(state_numbers)} \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 756c17dc3b3..14fdba013c7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -107,7 +107,12 @@ def make_custom_fields(update=True): dict(fieldname='gst_category', label='GST Category', fieldtype='Select', insert_after='gst_section', print_hide=1, options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - fetch_from='supplier.gst_category', fetch_if_empty=1) + fetch_from='supplier.gst_category', fetch_if_empty=1), + dict(fieldname='export_type', label='Export Type', + fieldtype='Select', insert_after='gst_category', print_hide=1, + depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type', + fetch_if_empty=1), ] sales_invoice_gst_category = [ @@ -116,20 +121,21 @@ def make_custom_fields(update=True): dict(fieldname='gst_category', label='GST Category', fieldtype='Select', insert_after='gst_section', print_hide=1, options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1) + fetch_from='customer.gst_category', fetch_if_empty=1), + dict(fieldname='export_type', label='Export Type', + fieldtype='Select', insert_after='gst_category', print_hide=1, + depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type', + fetch_if_empty=1), ] invoice_gst_fields = [ dict(fieldname='invoice_copy', label='Invoice Copy', - fieldtype='Select', insert_after='gst_category', print_hide=1, allow_on_submit=1, + fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), dict(fieldname='reverse_charge', label='Reverse Charge', fieldtype='Select', insert_after='invoice_copy', print_hide=1, options='Y\nN', default='N'), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='reverse_charge', print_hide=1, - depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax'), dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', fieldtype='Data', insert_after='export_type', print_hide=1), dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'), @@ -142,13 +148,13 @@ def make_custom_fields(update=True): purchase_invoice_gst_fields = [ dict(fieldname='supplier_gstin', label='Supplier GSTIN', fieldtype='Data', insert_after='supplier_address', - fetch_from='supplier_address.gstin', print_hide=1), + fetch_from='supplier_address.gstin', print_hide=1, read_only=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='shipping_address_display', - fetch_from='shipping_address.gstin', print_hide=1), + fetch_from='shipping_address.gstin', print_hide=1, read_only=1), dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='shipping_address', - print_hide=1, read_only=0), + print_hide=1, read_only=1), ] purchase_invoice_itc_fields = [ @@ -167,17 +173,17 @@ def make_custom_fields(update=True): sales_invoice_gst_fields = [ dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', + fieldtype='Data', insert_after='customer_address', read_only=1, fetch_from='customer_address.gstin', print_hide=1), dict(fieldname='customer_gstin', label='Customer GSTIN', fieldtype='Data', insert_after='shipping_address_name', fetch_from='shipping_address_name.gstin', print_hide=1), dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=0), + print_hide=1, read_only=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1), + fetch_from='company_address.gstin', print_hide=1, read_only=1), ] sales_invoice_shipping_fields = [ @@ -194,7 +200,11 @@ def make_custom_fields(update=True): inter_state_gst_field = [ dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1) + fieldtype='Check', insert_after='disabled', print_hide=1), + dict(fieldname='tax_category_column_break', fieldtype='Column Break', + insert_after='is_inter_state'), + dict(fieldname='gst_state', label='Source State', fieldtype='Select', + options='\n'.join(states), insert_after='company') ] ewaybill_fields = [ @@ -374,8 +384,7 @@ def make_custom_fields(update=True): 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, - 'Sales Taxes and Charges Template': inter_state_gst_field, - 'Purchase Taxes and Charges Template': inter_state_gst_field, + 'Tax Category': inter_state_gst_field, 'Item': [ dict(fieldname='gst_hsn_code', label='HSN/SAC', fieldtype='Link', options='GST HSN Code', insert_after='item_group'), @@ -459,6 +468,15 @@ def make_custom_fields(update=True): 'insert_after': 'gst_transporter_id', 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', 'default': 'Unregistered' + }, + { + 'fieldname': 'export_type', + 'label': 'Export Type', + 'fieldtype': 'Select', + 'insert_after': 'gst_category', + 'default': 'Without Payment of Tax', + 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ], 'Customer': [ @@ -469,6 +487,15 @@ def make_custom_fields(update=True): 'insert_after': 'customer_type', 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', 'default': 'Unregistered' + }, + { + 'fieldname': 'export_type', + 'label': 'Export Type', + 'fieldtype': 'Select', + 'insert_after': 'gst_category', + 'default': 'Without Payment of Tax', + 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ] } diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js new file mode 100644 index 00000000000..1e59032db10 --- /dev/null +++ b/erpnext/regional/india/taxes.js @@ -0,0 +1,41 @@ +erpnext.setup_auto_gst_taxation = (doctype) => { + frappe.ui.form.on(doctype, { + company_address: function(frm) { + frm.trigger('get_tax_template'); + }, + shipping_address: function(frm) { + frm.trigger('get_tax_template'); + }, + tax_category: function(frm) { + frm.trigger('get_tax_template'); + }, + get_tax_template: function(frm) { + let party_details = { + 'shipping_address': frm.doc.shipping_address || '', + 'shipping_address_name': frm.doc.shipping_address_name || '', + 'customer_address': frm.doc.customer_address || '', + 'customer': frm.doc.customer, + 'supplier': frm.doc.supplier, + 'supplier_gstin': frm.doc.supplier_gstin, + 'company_gstin': frm.doc.company_gstin, + 'tax_category': frm.doc.tax_category + }; + + frappe.call({ + method: 'erpnext.regional.india.utils.get_regional_address_details', + args: { + party_details: JSON.stringify(party_details), + doctype: frm.doc.doctype, + company: frm.doc.company, + return_taxes: 1 + }, + callback: function(r) { + if(r.message) { + frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + } + } + }); + } + }); +}; + diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index aae07797a15..0f9156a6b4c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -7,6 +7,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_ from erpnext.controllers.accounts_controller import get_taxes_and_charges 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 def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -46,6 +48,14 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) +def update_gst_category(doc, method): + for link in doc.links: + if link.link_doctype in ['Customer', 'Supplier']: + if doc.get('gstin'): + frappe.db.sql(""" + UPDATE `tab{0}` SET gst_category = %s WHERE name = %s AND gst_category = 'Unregistered' + """.format(link.link_doctype), ("Registered Regular", link.link_name)) #nosec + def set_gst_state_and_state_number(doc): if not doc.gst_state: if not doc.state: @@ -122,44 +132,108 @@ def test_method(): '''test function''' return 'overridden' -def get_place_of_supply(out, doctype): +def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): return - if doctype in ("Sales Invoice", "Delivery Note"): - address_name = out.shipping_address_name or out.customer_address - elif doctype == "Purchase Invoice": - address_name = out.shipping_address or out.supplier_address + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + address_name = party_details.shipping_address_name or party_details.customer_address + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + address_name = party_details.shipping_address or party_details.supplier_address if address_name: address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) if address and address.gst_state and address.gst_state_number: return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) -def get_regional_address_details(out, doctype, company): - out.place_of_supply = get_place_of_supply(out, doctype) +@frappe.whitelist() +def get_regional_address_details(party_details, doctype, company, return_taxes=None): - if not out.place_of_supply: return + if isinstance(party_details, string_types): + party_details = json.loads(party_details) + party_details = frappe._dict(party_details) - if doctype in ("Sales Invoice", "Delivery Note"): + party_details.place_of_supply = get_place_of_supply(party_details, doctype) + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - if not out.company_gstin: - return - elif doctype == "Purchase Invoice": - master_doctype = "Purchase Taxes and Charges Template" - if not out.supplier_gstin: + + get_tax_template_for_sez(party_details, master_doctype, company, 'Customer') + get_tax_template_based_on_category(master_doctype, company, party_details) + + if party_details.get('taxes_and_charges') and return_taxes: + return party_details + + if not party_details.company_gstin: return - if ((doctype in ("Sales Invoice", "Delivery Note") and out.company_gstin - and out.company_gstin[:2] != out.place_of_supply[:2]) or (doctype == "Purchase Invoice" - and out.supplier_gstin and out.supplier_gstin[:2] != out.place_of_supply[:2])): - default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0}) + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + master_doctype = "Purchase Taxes and Charges Template" + + get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier') + get_tax_template_based_on_category(master_doctype, company, party_details) + + if party_details.get('taxes_and_charges') and return_taxes: + return party_details + + if not party_details.supplier_gstin: + return + + if not party_details.place_of_supply: return + + if not party_details.company_gstin: return + + if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin + and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", + "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): + default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2]) else: - default_tax = frappe.db.get_value(master_doctype, {"company": company, "disabled":0, "is_default": 1}) + default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) if not default_tax: return - out["taxes_and_charges"] = default_tax - out.taxes = get_taxes_and_charges(master_doctype, default_tax) + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + + if return_taxes: + return party_details + +def get_tax_template_based_on_category(master_doctype, company, party_details): + if not party_details.get('tax_category'): + return + + default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, + 'name') + + if default_tax: + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + +def get_tax_template(master_doctype, company, is_inter_state, state_code): + tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], + filters = {'is_inter_state': is_inter_state}) + + default_tax = '' + + for tax_category in tax_categories: + if tax_category.gst_state == number_state_mapping[state_code] or \ + (not default_tax and not tax_category.gst_state): + default_tax = frappe.db.get_value(master_doctype, + {'disabled': 0, 'tax_category': tax_category.name}, 'name') + + return default_tax + +def get_tax_template_for_sez(party_details, master_doctype, company, party_type): + + gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))}, + ['gst_category', 'export_type'], as_dict=1) + + if gst_details: + if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax': + default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0, + "gst_state": number_state_mapping[party_details.company_gstin[:2]]}) + + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + def calculate_annual_eligible_hra_exemption(doc): basic_component = frappe.get_cached_value('Company', doc.company, "basic_component") @@ -555,7 +629,7 @@ def get_gst_accounts(company, account_wise=False): filters={"parent": "GST Settings", "company": company}, fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) - if not gst_settings_accounts: + if not gst_settings_accounts and not frappe.flags.in_test: frappe.throw(_("Please set GST Accounts in GST Settings")) for d in gst_settings_accounts: diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index ce559218cbd..1a7ff2bf5a6 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -55,14 +55,25 @@ frappe.query_reports["GSTR-1"] = { report.page.add_inner_button(__("Download as JSON"), function () { var filters = report.get_values(); - const args = { - cmd: 'erpnext.regional.report.gstr_1.gstr_1.get_json', - data: report.data, - report_name: report.report_name, - filters: filters - }; - - open_url_post(frappe.request.url, args); + frappe.call({ + method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.gstr_1.gstr_1.download_json_file', + data: r.message.data, + report_name: r.message.report_name, + report_type: r.message.report_type + }; + open_url_post(frappe.request.url, args); + } + } + }); }); } } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 090616b0777..4f9cc7ff7a0 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -532,16 +532,9 @@ class Gstr1Report(object): self.columns = self.invoice_columns + self.tax_columns + self.other_columns @frappe.whitelist() -def get_json(): - data = frappe._dict(frappe.local.form_dict) - - del data["cmd"] - if "csrf_token" in data: - del data["csrf_token"] - - filters = json.loads(data["filters"]) - report_data = json.loads(data["data"]) - report_name = data["report_name"] +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) gstin = get_company_gstin_number(filters["company"]) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) @@ -575,7 +568,11 @@ def get_json(): out = get_export_json(res) gst_json["exp"] = out - download_json_file(report_name, filters["type_of_business"], gst_json) + return { + 'report_name': report_name, + 'report_type': filters['type_of_business'], + 'data': gst_json + } def get_b2b_json(res, gstin): inv_type, out = {"Registered Regular": "R", "Deemed Export": "DE", "URD": "URD", "SEZ": "SEZ"}, [] @@ -722,11 +719,15 @@ def get_company_gstin_number(company): if gstin: return gstin[0]["gstin"] else: - frappe.throw(_("Please set valid GSTIN No. in Company Address")) + frappe.throw(_("Please set valid GSTIN No. in Company Address for company {0}".format( + frappe.bold(company) + ))) -def download_json_file(filename, report_type, data): +@frappe.whitelist() +def download_json_file(): ''' download json content in a file ''' - frappe.response['filename'] = frappe.scrub("{0} {1}".format(filename, report_type)) + '.json' - frappe.response['filecontent'] = json.dumps(data) + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0} {1}".format(data['report_name'], data['report_type'])) + '.json' + frappe.response['filecontent'] = data['data'] frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' diff --git a/erpnext/selling/doctype/sales_order/regional/india.js b/erpnext/selling/doctype/sales_order/regional/india.js new file mode 100644 index 00000000000..c11cfcc50b7 --- /dev/null +++ b/erpnext/selling/doctype/sales_order/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Sales Order'); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 85e81436d16..2dae0d8063d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -112,7 +112,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( let allow_delivery = false; if (doc.docstatus==1) { - this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); if(this.frm.has_perm("submit")) { if(doc.status === 'On Hold') { @@ -136,7 +135,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if(doc.status !== 'Closed') { if(doc.status !== 'On Hold') { - allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty)) + allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty)) && !this.frm.doc.skip_delivery_note if (this.frm.has_perm("submit")) { @@ -148,6 +147,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } + this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + // delivery note if(flt(doc.per_delivered, 6) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create')); @@ -202,7 +203,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } // payment request - if(flt(doc.per_billed)==0) { + if(flt(doc.per_billed)<100) { this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create')); this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create')); } @@ -361,7 +362,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, toggle_delivery_date: function() { - this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", + this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", (this.frm.doc.order_type == "Sales" && !this.frm.doc.skip_delivery_note)); }, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e97a4ee4611..2112a4174b1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -578,8 +578,12 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") - # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Delivery Note", 'company_address', target.company_address)) @@ -645,8 +649,12 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") - # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index bd078414885..feb6b76c4d3 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -12,6 +12,7 @@ from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate import json from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request +from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order class TestSalesOrder(unittest.TestCase): def tearDown(self): @@ -819,6 +820,25 @@ class TestSalesOrder(unittest.TestCase): mr_doc = frappe.get_doc('Material Request',mr.get('name')) self.assertEqual(mr_doc.items[0].sales_order, so.name) + def test_so_optional_blanket_order(self): + """ + Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. + Second Sales Order should not add on to Blanket Orders Ordered Quantity. + """ + + bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10) + + so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) + so_doc = frappe.get_doc('Sales Order', so.get('name')) + # To test if the SO has a Blanket Order + self.assertTrue(so_doc.items[0].blanket_order) + + so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) + so_doc = frappe.get_doc('Sales Order', so.get('name')) + # To test if the SO does NOT have a Blanket Order + self.assertEqual(so_doc.items[0].blanket_order, None) + + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) @@ -845,7 +865,8 @@ def make_sales_order(**args): "warehouse": args.warehouse, "qty": args.qty or 10, "uom": args.uom or None, - "rate": args.rate or 100 + "rate": args.rate or 100, + "against_blanket_order": args.against_blanket_order }) so.delivery_date = add_days(so.transaction_date, 10) 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 3fd1e6461e1..86b09c28148 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -68,6 +68,7 @@ "target_warehouse", "prevdoc_docname", "col_break4", + "against_blanket_order", "blanket_order", "blanket_order_rate", "planning_section", @@ -574,6 +575,7 @@ "report_hide": 1 }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", @@ -581,6 +583,7 @@ "options": "Blanket Order" }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", @@ -741,11 +744,17 @@ "fieldname": "image_section", "fieldtype": "Section Break", "label": "Image" + }, + { + "default": "0", + "fieldname": "against_blanket_order", + "fieldtype": "Check", + "label": "Against Blanket Order" } ], "idx": 1, "istable": 1, - "modified": "2019-10-10 08:46:26.244823", + "modified": "2019-11-19 14:19:29.491945", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index dc2c4ce4a96..c04bfd281e2 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -1,611 +1,158 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-06-25 10:25:16", - "custom": 0, - "description": "Settings for Selling Module", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 0, - "engine": "InnoDB", + "creation": "2013-06-25 10:25:16", + "description": "Settings for Selling Module", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "cust_master_name", + "campaign_naming_by", + "customer_group", + "territory", + "selling_price_list", + "close_opportunity_after_days", + "default_valid_till", + "column_break_5", + "so_required", + "dn_required", + "sales_update_frequency", + "maintain_same_sales_rate", + "editable_price_list_rate", + "allow_multiple_items", + "allow_against_multiple_purchase_orders", + "validate_selling_price", + "hide_tax_id" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Customer Name", - "fieldname": "cust_master_name", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Customer Naming By", - "length": 0, - "no_copy": 0, - "options": "Customer Name\nNaming Series", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Customer Name", + "fieldname": "cust_master_name", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Customer Naming By", + "options": "Customer Name\nNaming Series" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "campaign_naming_by", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Campaign Naming By", - "length": 0, - "no_copy": 0, - "options": "Campaign Name\nNaming Series", - "permlevel": 0, - "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": "campaign_naming_by", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "customer_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "permlevel": 0, - "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": "customer_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Customer Group", + "options": "Customer Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "territory", - "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": "Default Territory", - "length": 0, - "no_copy": 0, - "options": "Territory", - "permlevel": 0, - "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": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Territory", + "options": "Territory" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_price_list", - "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": "Default Price List", - "length": 0, - "no_copy": 0, - "options": "Price List", - "permlevel": 0, - "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": "selling_price_list", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Price List", + "options": "Price List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "15", - "description": "Auto close Opportunity after 15 days", - "fieldname": "close_opportunity_after_days", - "fieldtype": "Int", - "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": "Close Opportunity After Days", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "15", + "description": "Auto close Opportunity after 15 days", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Opportunity After Days" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_valid_till", - "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": "Default Quotation Validity Days", - "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": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "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, - "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_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "so_required", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Order Required", - "length": 0, - "no_copy": 0, - "options": "No\nYes", - "permlevel": 0, - "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": "so_required", + "fieldtype": "Select", + "label": "Sales Order Required", + "options": "No\nYes" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dn_required", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Delivery Note Required", - "length": 0, - "no_copy": 0, - "options": "No\nYes", - "permlevel": 0, - "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": "dn_required", + "fieldtype": "Select", + "label": "Delivery Note Required", + "options": "No\nYes" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Each Transaction", - "description": "How often should project and company be updated based on Sales Transactions.", - "fieldname": "sales_update_frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Update Frequency", - "length": 0, - "no_copy": 0, - "options": "Each Transaction\nDaily\nMonthly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Each Transaction", + "description": "How often should project and company be updated based on Sales Transactions.", + "fieldname": "sales_update_frequency", + "fieldtype": "Select", + "label": "Sales Update Frequency", + "options": "Each Transaction\nDaily\nMonthly", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintain_same_sales_rate", - "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": "Maintain Same Rate Throughout Sales Cycle", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "maintain_same_sales_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout Sales Cycle" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "editable_price_list_rate", - "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": "Allow user to edit Price List Rate in transactions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "editable_price_list_rate", + "fieldtype": "Check", + "label": "Allow user to edit Price List Rate in transactions" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_multiple_items", - "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": "Allow Item to be added multiple times in a transaction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "allow_multiple_items", + "fieldtype": "Check", + "label": "Allow Item to be added multiple times in a transaction" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_against_multiple_purchase_orders", - "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": "Allow multiple Sales Orders against a Customer's Purchase Order", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "allow_against_multiple_purchase_orders", + "fieldtype": "Check", + "label": "Allow multiple Sales Orders against a Customer's Purchase Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "validate_selling_price", - "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": "Validate Selling Price for Item against Purchase Rate or Valuation Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "validate_selling_price", + "fieldtype": "Check", + "label": "Validate Selling Price for Item against Purchase Rate or Valuation Rate" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hide_tax_id", - "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": "Hide Customer's Tax Id from Sales Transactions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "fieldname": "hide_tax_id", + "fieldtype": "Check", + "label": "Hide Customer's Tax Id from Sales Transactions" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-cog", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2018-06-25 12:56:16.332039", - "modified_by": "Administrator", - "module": "Selling", - "name": "Selling Settings", - "owner": "Administrator", + ], + "icon": "fa fa-cog", + "idx": 1, + "issingle": 1, + "modified": "2019-12-09 13:38:36.486298", + "modified_by": "Administrator", + "module": "Selling", + "name": "Selling Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/test_selling_settings.py b/erpnext/selling/doctype/selling_settings/test_selling_settings.py new file mode 100644 index 00000000000..961a54de0a8 --- /dev/null +++ b/erpnext/selling/doctype/selling_settings/test_selling_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSellingSettings(unittest.TestCase): + pass diff --git a/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json similarity index 77% rename from erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json rename to erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json index a0bb6fe26df..f39fea48960 100644 --- a/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json +++ b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json @@ -3,18 +3,18 @@ "app": "ERPNext", "creation": "2019-11-15 14:44:10.065014", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [ { - "label": "Customers", + "label": "Learn More", "video_id": "zsrrVDk6VBs" } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/customer.png", + "image_src": "/assets/erpnext/images/illustrations/customers-onboard.png", "max_count": 3, - "modified": "2019-11-26 18:26:15.888794", + "modified": "2019-12-03 22:54:28.959549", "modified_by": "Administrator", "name": "Add A Few Customers", "owner": "Administrator", @@ -44,6 +44,5 @@ ], "slide_order": 40, "slide_title": "Add A Few Customers", - "slide_type": "Create", - "submit_method": "" + "slide_type": "Create" } \ No newline at end of file diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index 32711b2fce0..056492a3274 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -7,7 +7,7 @@ from frappe.utils import flt def execute(filters=None): if not filters: filters = {} - + columns = get_columns() iwq_map = get_item_warehouse_quantity_map() item_map = get_item_details() @@ -15,22 +15,23 @@ def execute(filters=None): for sbom, warehouse in iwq_map.items(): total = 0 total_qty = 0 - + for wh, item_qty in warehouse.items(): total += 1 - row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, - item_map.get(sbom).stock_uom, wh] - available_qty = item_qty - total_qty += flt(available_qty) - row += [available_qty] - - if available_qty: - data.append(row) - if (total == len(warehouse)): - row = ["", "", "Total", "", "", total_qty] + if item_map.get(sbom): + row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, + item_map.get(sbom).stock_uom, wh] + available_qty = item_qty + total_qty += flt(available_qty) + row += [available_qty] + + if available_qty: data.append(row) + if (total == len(warehouse)): + row = ["", "", "Total", "", "", total_qty] + data.append(row) return columns, data - + def get_columns(): columns = ["Item Code:Link/Item:100", "Item Name::100", "Description::120", \ "UOM:Link/UOM:80", "Warehouse:Link/Warehouse:100", "Quantity::100"] diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js index ee806a78fb4..daca2e3bd0c 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js @@ -20,11 +20,15 @@ frappe.query_reports["Item-wise Sales History"] = { }, { fieldname:"from_date", + reqd: 1, label: __("From Date"), fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { fieldname:"to_date", + reqd: 1, + default: frappe.datetime.get_today(), label: __("To Date"), fieldtype: "Date", }, 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 226c34f735a..1fc3663bed7 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 @@ -196,6 +196,7 @@ def get_customer_details(): def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) + return frappe.db.sql(""" SELECT so_item.item_code, so_item.item_name, so_item.item_group, @@ -208,7 +209,6 @@ def get_sales_order_details(company_list, filters): `tabSales Order` so, `tabSales Order Item` so_item WHERE so.name = so_item.parent - AND so.company in (%s) - AND so.docstatus = 1 - {0} - """.format(conditions), company_list, as_dict=1) #nosec + AND so.company in ({0}) + AND so.docstatus = 1 {1} + """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 04e8a83e7f5..2eee919b530 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -14,6 +14,8 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils.nestedset import NestedSet +import functools + class Company(NestedSet): nsm_parent_field = 'parent_company' @@ -560,3 +562,26 @@ def get_timeline_data(doctype, name): return json.loads(history) if history and '{' in history else {} return date_to_value_dict + +@frappe.whitelist() +def get_default_company_address(name, sort_key='is_primary_address', existing_address=None): + if sort_key not in ['is_shipping_address', 'is_primary_address']: + return None + + out = frappe.db.sql(""" SELECT + addr.name, addr.%s + FROM + `tabAddress` addr, `tabDynamic Link` dl + WHERE + dl.parent = addr.name and dl.link_doctype = 'Company' and + dl.link_name = %s and ifnull(addr.disabled, 0) = 0 + """ %(sort_key, '%s'), (name)) #nosec + + if existing_address: + if existing_address in [d[0] for d in out]: + return existing_address + + if out: + return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0] + else: + return None \ No newline at end of file diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 0bcddc21517..4d2d540bbc8 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -283,7 +283,7 @@ class EmailDigest(Document): card.value = card.value *-1 card.value = self.fmt_money(card.value,False if key in ("bank_balance", "credit_balance") else True) - cache.setex(cache_key, card, 24 * 60 * 60) + cache.set_value(cache_key, card, expires_in_sec=24 * 60 * 60) context.cards.append(card) diff --git a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json new file mode 100644 index 00000000000..bf330d09de4 --- /dev/null +++ b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json @@ -0,0 +1,23 @@ +{ + "add_more_button": 0, + "app": "ERPNext", + "creation": "2019-12-04 19:21:39.995776", + "docstatus": 0, + "doctype": "Onboarding Slide", + "domains": [], + "help_links": [], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/desk-onboard.png", + "is_completed": 0, + "max_count": 3, + "modified": "2019-12-04 19:21:39.995776", + "modified_by": "Administrator", + "name": "Welcome back to ERPNext!", + "owner": "Administrator", + "slide_desc": " Let's continue where you left from! ", + "slide_fields": [], + "slide_module": "Setup", + "slide_order": 0, + "slide_title": "Welcome back to ERPNext!", + "slide_type": "Continue" +} \ No newline at end of file diff --git a/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json similarity index 60% rename from erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json rename to erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json index 1da9dd44e2b..4ea69852afa 100644 --- a/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json +++ b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json @@ -3,20 +3,20 @@ "app": "ERPNext", "creation": "2019-11-26 17:01:26.671859", "docstatus": 0, - "doctype": "Setup Wizard Slide", + "doctype": "Onboarding Slide", "domains": [], "help_links": [], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/onboard.png", + "image_src": "/assets/erpnext/images/illustrations/desk-onboard.png", "max_count": 0, - "modified": "2019-11-26 17:17:29.813299", + "modified": "2019-12-03 22:49:12.871260", "modified_by": "Administrator", "name": "Welcome to ERPNext!", "owner": "Administrator", - "slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!", + "slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!", "slide_fields": [], "slide_module": "Setup", - "slide_order": 10, + "slide_order": 1, "slide_title": "Welcome to ERPNext!", "slide_type": "Information" } \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/test_setup_wizard.py b/erpnext/setup/setup_wizard/test_setup_wizard.py deleted file mode 100644 index a489133abae..00000000000 --- a/erpnext/setup/setup_wizard/test_setup_wizard.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals - -import frappe, time -from frappe.utils.selenium_testdriver import TestDriver - -def run_setup_wizard_test(): - driver = TestDriver() - frappe.db.set_default('in_selenium', '1') - frappe.db.commit() - - driver.login('#page-setup-wizard') - print('Running Setup Wizard Test...') - - # Language slide - driver.wait_for_ajax(True) - time.sleep(1) - - driver.set_select("language", "English (United States)") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - - # Region slide - driver.wait_for_ajax(True) - driver.set_select("country", "India") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - - # Profile slide - driver.set_field("full_name", "Great Tester") - driver.set_field("email", "great@example.com") - driver.set_field("password", "test") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - time.sleep(1) - - # domain slide - driver.set_multicheck("domains", ["Manufacturing"]) - time.sleep(1) - driver.click(".next-btn") - - # Org slide - driver.set_field("company_name", "For Testing") - time.sleep(1) - driver.print_console() - driver.click(".next-btn") - - driver.set_field("company_tagline", "Just for GST") - driver.set_field("bank_account", "HDFC") - time.sleep(3) - driver.click(".complete-btn") - - # Wait for desktop - driver.wait_for('#page-desktop', timeout=600) - - driver.print_console() - time.sleep(3) - - frappe.db.set_default('in_selenium', None) - frappe.db.set_value("Company", "For Testing", "write_off_account", "Write Off - FT") - frappe.db.set_value("Company", "For Testing", "exchange_gain_loss_account", "Exchange Gain/Loss - FT") - frappe.db.commit() - - driver.close() - - return True diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index d1c206d8b1d..1a86b79ad10 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -106,7 +106,8 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No # expire in 6 hours response.raise_for_status() value = response.json()["rates"][to_currency] - cache.setex(key, value, 6 * 60 * 60) + + cache.set_value(key, value, expires_in_sec=6 * 60 * 60) return flt(value) except: frappe.log_error(title="Get Exchange Rate") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c98dfe35fb5..39aad2e0071 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -424,7 +424,12 @@ def make_sales_invoice(source_name, target_doc=None): target.run_method("calculate_taxes_and_totals") # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js new file mode 100644 index 00000000000..22f4716ea52 --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -0,0 +1,4 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Delivery Note'); + diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 831381c86ad..5341f298531 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -65,7 +65,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip bin = get_bin_qty(packing_item_code, pi.warehouse) pi.actual_qty = flt(bin.get("actual_qty")) pi.projected_qty = flt(bin.get("projected_qty")) - if old_packed_items_map: + if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)): pi.batch_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].batch_no pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d0fae6a2272..060175f9045 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -95,7 +95,8 @@ class PurchaseReceipt(BuyingController): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ + company = self.company) break def validate_with_previous_doc(self): @@ -244,7 +245,7 @@ class PurchaseReceipt(BuyingController): negative_expense_to_be_booked += flt(d.item_tax_amount) # Amount added through landed-cost-voucher - if landed_cost_entries: + if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): gl_entries.append(self.get_gl_dict({ "account": account, @@ -364,8 +365,9 @@ class PurchaseReceipt(BuyingController): def add_asset_gl_entries(self, item, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") - # This returns company's default cwip account - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + # This returns category's cwip account if not then fallback to company's default cwip account + cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ + company = self.company) asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) @@ -609,7 +611,7 @@ def make_stock_entry(source_name,target_doc=None): def get_item_account_wise_additional_cost(purchase_document): landed_cost_voucher = frappe.get_value("Landed Cost Purchase Receipt", - {"receipt_document": purchase_document}, "parent") + {"receipt_document": purchase_document, "docstatus": 1}, "parent") if not landed_cost_voucher: return @@ -620,8 +622,7 @@ def get_item_account_wise_additional_cost(purchase_document): based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) for item in landed_cost_voucher_doc.items: - if item.receipt_document == purchase_document: - total_item_cost += item.get(based_on_field) + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: diff --git a/erpnext/stock/doctype/purchase_receipt/regional/india.js b/erpnext/stock/doctype/purchase_receipt/regional/india.js new file mode 100644 index 00000000000..b4f1201f36c --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Receipt'); \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 19eb398130c..23d00da7c1f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -353,17 +353,19 @@ def get_auto_serial_nos(serial_no_series, qty): def auto_make_serial_nos(args): serial_nos = get_serial_nos(args.get('serial_no')) created_numbers = [] + voucher_type = args.get('voucher_type') + item_code = args.get('item_code') for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): sr = frappe.get_doc("Serial No", serial_no) sr.via_stock_ledger = True - sr.item_code = args.get('item_code') + sr.item_code = item_code sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None sr.batch_no = args.get('batch_no') sr.location = args.get('location') sr.company = args.get('company') sr.supplier = args.get('supplier') - if sr.sales_order and args.get('voucher_type') == "Stock Entry" \ + if sr.sales_order and voucher_type == "Stock Entry" \ and not args.get('actual_qty', 0) > 0: sr.sales_order = None sr.save(ignore_permissions=True) @@ -371,10 +373,28 @@ def auto_make_serial_nos(args): created_numbers.append(make_serial_no(serial_no, args)) form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) + + # Setting up tranlated title field for all cases + singular_title = _("Serial Number Created") + multiple_title = _("Serial Numbers Created") + + if voucher_type: + multiple_title = singular_title = _("{0} Created").format(voucher_type) + if len(form_links) == 1: - frappe.msgprint(_("Serial No {0} created").format(form_links[0])) + frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title) elif len(form_links) > 0: - frappe.msgprint(_("The following serial numbers were created: {0}").format(', '.join(form_links))) + message = _("The following serial numbers were created: {0}").format(get_items_html(form_links, item_code)) + frappe.msgprint(message, multiple_title) + +def get_items_html(serial_nos, item_code): + body = ', '.join(serial_nos) + return ''' + {0}: {1} Serial Numbers ++{2} |