From 2744765757cfeb9a2f9e9671d91e945ad4b6bcaf Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 7 Sep 2020 12:31:19 +0530 Subject: [PATCH 001/229] feat(UAE VAT Format): add fields for emirates in address and sales invoice --- erpnext/regional/united_arab_emirates/setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 250659e54d..a2938bb300 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -41,6 +41,8 @@ def make_custom_fields(): dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), + dict(fieldname='emirate', label='Emirate', insert_after='customer_address', + fetch_from='customer_address.emirates'), ] invoice_item_fields = [ @@ -76,6 +78,10 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Data', insert_after='supplier_name'), ], + 'Address': [ + dict(fieldname='emirates', label='Emirates', + fieldtype='Data', insert_after='state'), + ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields, 'Purchase Receipt': purchase_invoice_fields + invoice_fields, From 7b7a8e1309ba0c262da7de07424858f5de9751d3 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Wed, 9 Sep 2020 20:54:30 +0530 Subject: [PATCH 002/229] feat: RCM for UAE VAT --- erpnext/hooks.py | 8 +- .../regional/united_arab_emirates/utils.py | 144 +++++++++++++++++- 2 files changed, 148 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 463ad6c94b..72be2dcc75 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -246,7 +246,10 @@ doc_events = { "on_trash": "erpnext.regional.check_deletion_permission" }, "Purchase Invoice": { - "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" + "validate": [ + "erpnext.regional.india.utils.update_grand_total_for_rcm", + "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", + ] }, "Payment Entry": { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], @@ -379,7 +382,8 @@ regional_overrides = { 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries' }, 'United Arab Emirates': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' + 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', + 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries', }, 'Saudi Arabia': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index a0425f6b1c..05a99d190a 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt +from frappe import _ +import erpnext +from frappe.utils import flt, round_based_on_smallest_currency_fraction, money_in_words from erpnext.controllers.taxes_and_totals import get_itemised_tax from six import iteritems @@ -26,4 +28,142 @@ def update_itemised_tax_data(doc): row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) - row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) \ No newline at end of file + row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + +def get_account_currency(account): + """Helper function to get account currency""" + if not account: + return + def generator(): + account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"]) + if not account_currency: + account_currency = frappe.get_cached_value('Company', company, "default_currency") + + return account_currency + + return frappe.local_cache("account_currency", account, generator) + +def get_tax_accounts(company): + """Get the list of tax accounts for a specific company + + Args: + company (String): Current Company set as default + + Returns: + tax_accounts: List of Tax Accounts for the company + """ + tax_accounts_dict = frappe._dict() + tax_accounts_list = frappe.get_all("Account", + filters={"account_type": "Tax", "company": company}, + fields=["name"]) + + if not tax_accounts_list and not frappe.flags.in_test: + frappe.throw(_("Please create at least one Account of type Tax")) + for d in tax_accounts_list: + for key, name in d.items(): + tax_accounts_dict[name] = name + + return tax_accounts_dict + +def update_grand_total_for_rcm(doc, method): + """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form + + Args: + doc (Document): The document for the current Purchase Invoice + """ + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'United Arab Emirates': + return + + if not doc.total_taxes_and_charges: + return + + if doc.reverse_charge == 'Y': + tax_accounts = get_tax_accounts(doc.company) + + base_vat_tax = 0 + vat_tax = 0 + + for tax in doc.get('taxes'): + if tax.category not in ("Total", "Valuation and Total"): + continue + + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: + base_vat_tax += tax.base_tax_amount_after_discount_amount + vat_tax += tax.tax_amount_after_discount_amount + + doc.taxes_and_charges_added -= vat_tax + doc.total_taxes_and_charges -= vat_tax + doc.base_taxes_and_charges_added -= base_vat_tax + doc.base_total_taxes_and_charges -= base_vat_tax + + update_totals(vat_tax, base_vat_tax, doc) + +def update_totals(vat_tax, base_vat_tax, doc): + """Update the grand total values in the form + + Args: + vat_tax (float): Vat Tax to be subtracted + base_vat_tax (float): Base Vat Tax to be subtracted + doc (Document): The document for the current Purchase Invoice + """ + + doc.base_grand_total -= base_vat_tax + doc.grand_total -= vat_tax + + if doc.meta.get_field("rounded_total"): + + if doc.is_rounded_total_disabled(): + doc.outstanding_amount = doc.grand_total + + else: + doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, + doc.currency, doc.precision("rounded_total")) + doc.rounding_adjustment = flt(doc.rounded_total - doc.grand_total, + doc.precision("rounding_adjustment")) + doc.outstanding_amount = doc.rounded_total or doc.grand_total + + doc.in_words = money_in_words(doc.grand_total, doc.currency) + doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) + doc.set_payment_schedule() + +def make_regional_gl_entries(gl_entries, doc): + """This method is hooked to the make_regional_gl_entries in Purchase Invoice. + It appends the region specific general ledger entries to the list of GL Entries. + + Args: + gl_entries (List): List of GL entries to be made + doc (Document): The document for the current Purchase Invoice + + Returns: + List: Updates list of GL Entries + """ + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'United Arab Emirates': + return gl_entries + + if doc.reverse_charge == 'Y': + tax_accounts = get_tax_accounts(doc.company) + for tax in doc.get('taxes'): + if tax.category not in ("Total", "Valuation and Total"): + continue + + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: + account_currency = get_account_currency(tax.account_head) + + gl_entries.append(doc.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "posting_date": doc.posting_date, + "against": doc.supplier, + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ + if account_currency==doc.company_currency \ + else tax.tax_amount_after_discount_amount + }, account_currency, item=tax) + ) + return gl_entries \ No newline at end of file From 91f1e266726c20abd2afc797c0c12f0e8793a3c5 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 10 Sep 2020 13:55:14 +0530 Subject: [PATCH 003/229] feat: Add rated supplies rows in UAE VAT report --- erpnext/regional/report/uae_vat/__init__.py | 0 erpnext/regional/report/uae_vat/uae_vat.js | 31 ++++++ erpnext/regional/report/uae_vat/uae_vat.json | 22 ++++ erpnext/regional/report/uae_vat/uae_vat.py | 108 +++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 erpnext/regional/report/uae_vat/__init__.py create mode 100644 erpnext/regional/report/uae_vat/uae_vat.js create mode 100644 erpnext/regional/report/uae_vat/uae_vat.json create mode 100644 erpnext/regional/report/uae_vat/uae_vat.py diff --git a/erpnext/regional/report/uae_vat/__init__.py b/erpnext/regional/report/uae_vat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/report/uae_vat/uae_vat.js b/erpnext/regional/report/uae_vat/uae_vat.js new file mode 100644 index 0000000000..45df167823 --- /dev/null +++ b/erpnext/regional/report/uae_vat/uae_vat.js @@ -0,0 +1,31 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["UAE VAT"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -3), + "width": "80" + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + ] +}; diff --git a/erpnext/regional/report/uae_vat/uae_vat.json b/erpnext/regional/report/uae_vat/uae_vat.json new file mode 100644 index 0000000000..8807405a98 --- /dev/null +++ b/erpnext/regional/report/uae_vat/uae_vat.json @@ -0,0 +1,22 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-09-10 08:51:02.298482", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-09-10 08:51:02.298482", + "modified_by": "Administrator", + "module": "Regional", + "name": "UAE VAT", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "UAE VAT", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat/uae_vat.py new file mode 100644 index 0000000000..6e9565b1e5 --- /dev/null +++ b/erpnext/regional/report/uae_vat/uae_vat.py @@ -0,0 +1,108 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_columns(): + return [ + { + "fieldname": "no", + "label": "No", + "fieldtype": "Data", + "width": 50 + }, + { + "fieldname": "legend", + "label": "Legend", + "fieldtype": "Data", + "width": 300 + }, + { + "fieldname": "amount", + "label": "Amount (AED)", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "vat_amount", + "label": "VAT Amount (AED)", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "adjustment", + "label": "Adjustment (AED)", + "fieldtype": "Currency", + "width": 100 + } + ] + +def get_data(filters = None): + data = [] + total_emiratewise = get_total_emiratewise(filters) + emirates = get_emirates() + amounts_by_emirate = {} + for d in total_emiratewise: + emirate, amount, vat= d + amounts_by_emirate[emirate] = { + "legend": emirate, + "amount": amount, + "vat_amount": vat + } + for d, emirate in enumerate(emirates, 97): + if emirate in amounts_by_emirate: + amounts_by_emirate[emirate]["no"] = f'1{chr(d)}' + amounts_by_emirate[emirate]["legend"] = f'Standard rated supplies in {emirate}' + data.append(amounts_by_emirate[emirate]) + else: + data.append( + { + "no": f'1{chr(d)}', + "legend": f'Standard rated supplies in {emirate}', + "amount": 0, + "vat_amount": 0 + } + ) + return data + + +def get_total_emiratewise(filters): + conditions = get_conditions(filters) + print(f""" + select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` + where docstatus = 1 {conditions} + group by `tabSales Invoice`.emirate; + """) + return frappe.db.sql(f""" + select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` + where docstatus = 1 {conditions} + group by `tabSales Invoice`.emirate; + """, filters) + +def get_emirates(): + return [ + 'Abu Dhabi', + 'Dubai', + 'Sharjah', + 'Ajman', + 'Umm Al Quwain', + 'Ras Al Khaimah', + 'Fujairah' + ] + +def get_conditions(filters): + conditions = "" + + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): + if filters.get(opts[0]): + conditions += opts[1] + return conditions \ No newline at end of file From 031d77be2e58a033786a81354f08607ea07e4910 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 10 Sep 2020 17:40:01 +0530 Subject: [PATCH 004/229] feat: add reverse charge to uae vat report --- erpnext/regional/report/uae_vat/uae_vat.py | 53 +++++++++++++++---- .../regional/united_arab_emirates/setup.py | 8 +-- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat/uae_vat.py index 6e9565b1e5..50d1ddd351 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.py +++ b/erpnext/regional/report/uae_vat/uae_vat.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from erpnext.regional.united_arab_emirates.utils import get_tax_accounts def execute(filters=None): columns = get_columns() @@ -70,19 +71,21 @@ def get_data(filters = None): "vat_amount": 0 } ) + data.append( + { + "no": '3', + "legend": f'Supplies subject to the reverse charge provision', + "amount": get_reverse_charge_total(filters), + "vat_amount": get_reverse_charge_tax(filters) + } + ) return data def get_total_emiratewise(filters): - conditions = get_conditions(filters) - print(f""" - select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` - where docstatus = 1 {conditions} - group by `tabSales Invoice`.emirate; - """) return frappe.db.sql(f""" select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` - where docstatus = 1 {conditions} + where docstatus = 1 {get_conditions(filters)} group by `tabSales Invoice`.emirate; """, filters) @@ -99,10 +102,40 @@ def get_emirates(): def get_conditions(filters): conditions = "" + for opts in (("company", f' and company="{filters.get("company")}"'), + ("from_date", f' and posting_date>="{filters.get("from_date")}"'), + ("to_date", f' and posting_date<="{filters.get("to_date")}"')): + if filters.get(opts[0]): + conditions += opts[1] + return conditions - for opts in (("company", " and company=%(company)s"), - ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): +def get_reverse_charge_tax(filters): + return frappe.db.sql(f""" + select sum(debit) from + `tabPurchase Invoice` inner join `tabGL Entry` + on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name + where + `tabPurchase Invoice`.reverse_charge = "Y" + and `tabPurchase Invoice`.docstatus = 1 + and `tabGL Entry`.docstatus = 1 {get_conditions_join(filters)} + and account in ("{'", "'.join(get_tax_accounts(filters['company']))}"); + """)[0][0] + + +def get_reverse_charge_total(filters): + return frappe.db.sql(f""" + select sum(total) from + `tabPurchase Invoice` + where + reverse_charge = "Y" + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] + +def get_conditions_join(filters): + conditions = "" + for opts in (("company", f' and `tabPurchase Invoice`.company="{filters.get("company")}"'), + ("from_date", f' and `tabPurchase Invoice`.posting_date>="{filters.get("from_date")}"'), + ("to_date", f' and `tabPurchase Invoice`.posting_date<="{filters.get("to_date")}"')): if filters.get(opts[0]): conditions += opts[1] return conditions \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index a2938bb300..d38d647277 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -20,7 +20,7 @@ def make_custom_fields(): insert_after='group_same_items', print_hide=1, collapsible=1), dict(fieldname='permit_no', label='Permit Number', fieldtype='Data', insert_after='vat_section', print_hide=1), - dict(fieldname='reverse_charge_applicable', label='Reverse Charge Applicable', + dict(fieldname='reverse_charge', label='Reverse Charge Applicable', fieldtype='Select', insert_after='permit_no', print_hide=1, options='Y\nN', default='N') ] @@ -42,7 +42,7 @@ def make_custom_fields(): fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), dict(fieldname='emirate', label='Emirate', insert_after='customer_address', - fetch_from='customer_address.emirates'), + fieldtype='Read Only', fetch_from='customer_address.emirates'), ] invoice_item_fields = [ @@ -79,8 +79,8 @@ def make_custom_fields(): fieldtype='Data', insert_after='supplier_name'), ], 'Address': [ - dict(fieldname='emirates', label='Emirates', - fieldtype='Data', insert_after='state'), + dict(fieldname='emirates', label='Emirates', fieldtype='Select', insert_after='state', + options='Abu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields, From 323791d123e63e9a7b61109d7a727137e7fe81d0 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 10 Sep 2020 17:53:29 +0530 Subject: [PATCH 005/229] refactor: added docstrings --- erpnext/regional/report/uae_vat/uae_vat.py | 75 +++++++++++++++++++--- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat/uae_vat.py index 50d1ddd351..4c4c6bc2ab 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.py +++ b/erpnext/regional/report/uae_vat/uae_vat.py @@ -12,6 +12,11 @@ def execute(filters=None): return columns, data def get_columns(): + """Creates a list of dictionaries that are used to generate column headers of the data table + + Returns: + List(Dict): list of dictionaries that are used to generate column headers of the data table + """ return [ { "fieldname": "no", @@ -46,6 +51,14 @@ def get_columns(): ] def get_data(filters = None): + """Returns the list of dictionaries. Each dictionary is a row in the datatable + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + List(Dict): Each dictionary is a row in the datatable + """ data = [] total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() @@ -90,6 +103,11 @@ def get_total_emiratewise(filters): """, filters) def get_emirates(): + """Returns a List of emirates in the order that they are to be displayed + + Returns: + List(String): List of emirates in the order that they are to be displayed + """ return [ 'Abu Dhabi', 'Dubai', @@ -101,6 +119,14 @@ def get_emirates(): ] def get_conditions(filters): + """The conditions to be used to filter data to calculate the total sale + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + String: Concatenated list of conditions to be applied to calculate the total sale + """ conditions = "" for opts in (("company", f' and company="{filters.get("company")}"'), ("from_date", f' and posting_date>="{filters.get("from_date")}"'), @@ -109,7 +135,40 @@ def get_conditions(filters): conditions += opts[1] return conditions +def get_reverse_charge_total(filters): + """Returns the sum of the total of each Purchase invoice made + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the total of each Purchase invoice made + """ + conditions = """ + for opts in (("company", f' and company="{filters.get("company")}"'), + ("from_date", f' and posting_date>="{filters.get("from_date")}"'), + ("to_date", f' and posting_date<="{filters.get("to_date")}"')): + if filters.get(opts[0]): + conditions += opts[1] + return conditions + """ + return frappe.db.sql(f""" + select sum(total) from + `tabPurchase Invoice` + where + reverse_charge = "Y" + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] + def get_reverse_charge_tax(filters): + """Returns the sum of the tax of each Purchase invoice made + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the tax of each Purchase invoice made + """ return frappe.db.sql(f""" select sum(debit) from `tabPurchase Invoice` inner join `tabGL Entry` @@ -122,16 +181,16 @@ def get_reverse_charge_tax(filters): """)[0][0] -def get_reverse_charge_total(filters): - return frappe.db.sql(f""" - select sum(total) from - `tabPurchase Invoice` - where - reverse_charge = "Y" - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] def get_conditions_join(filters): + """The conditions to be used to filter data to calculate the total vat + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + String: Concatenated list of conditions to be applied to calculate the total vat + """ conditions = "" for opts in (("company", f' and `tabPurchase Invoice`.company="{filters.get("company")}"'), ("from_date", f' and `tabPurchase Invoice`.posting_date>="{filters.get("from_date")}"'), From f44524dd3e3ed87ed112d99ea7fef8cf450c9dcd Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 10 Sep 2020 18:35:04 +0530 Subject: [PATCH 006/229] feat: Added a Chart to compare Vat and sales between emirates --- erpnext/regional/report/uae_vat/uae_vat.py | 48 +++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat/uae_vat.py index 4c4c6bc2ab..8eeff51723 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.py +++ b/erpnext/regional/report/uae_vat/uae_vat.py @@ -4,12 +4,12 @@ from __future__ import unicode_literals import frappe from erpnext.regional.united_arab_emirates.utils import get_tax_accounts +from frappe import _ def execute(filters=None): columns = get_columns() - data = get_data(filters) - - return columns, data + data, chart = get_data(filters) + return columns, data, None, chart def get_columns(): """Creates a list of dictionaries that are used to generate column headers of the data table @@ -51,13 +51,14 @@ def get_columns(): ] def get_data(filters = None): - """Returns the list of dictionaries. Each dictionary is a row in the datatable + """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data Args: filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. Returns: List(Dict): Each dictionary is a row in the datatable + Dict: Dictionary containing chart data """ data = [] total_emiratewise = get_total_emiratewise(filters) @@ -70,6 +71,9 @@ def get_data(filters = None): "amount": amount, "vat_amount": vat } + + chart = get_chart_data(emirates, amounts_by_emirate) + for d, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: amounts_by_emirate[emirate]["no"] = f'1{chr(d)}' @@ -92,9 +96,43 @@ def get_data(filters = None): "vat_amount": get_reverse_charge_tax(filters) } ) - return data + return data, chart +def get_chart_data(emirates, amounts_by_emirate): + """Returns chart data + + Args: + emirates (List): List of Emirates + amounts_by_emirate (Dict): Vat and Tax amount by emirates with emirates as key + + Returns: + [Dict]: Chart Data + """ + labels = [] + amount = [] + vat_amount = [] + for d in emirates: + if d in amounts_by_emirate: + amount.append(amounts_by_emirate[d]["amount"]) + vat_amount.append(amounts_by_emirate[d]["vat_amount"]) + labels.append(d) + + datasets = [] + datasets.append({'name': _('Amount (AED)'), 'values': amount}) + datasets.append({'name': _('Vat Amount (AED)'), 'values': vat_amount}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + } + } + + chart["type"] = "bar" + chart["fieldtype"] = "Currency" + return chart + def get_total_emiratewise(filters): return frappe.db.sql(f""" select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` From 3ef11b1d647666b2eed836fd7f449e1b1bb88f8f Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 24 Sep 2020 13:21:23 +0530 Subject: [PATCH 007/229] feat(UAE VAT 21): Add region fields for UAE VAT 21 --- erpnext/hooks.py | 3 +++ .../regional/united_arab_emirates/setup.py | 20 +++++++++++++++---- .../regional/united_arab_emirates/utils.py | 12 ++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 72be2dcc75..ec34c4e126 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -243,6 +243,9 @@ doc_events = { "erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.erpnext_integrations.taxjar_integration.delete_transaction" ], + "validate": [ + "erpnext.regional.united_arab_emirates.utils.validate_returns", + ], "on_trash": "erpnext.regional.check_deletion_permission" }, "Purchase Invoice": { diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index d38d647277..f116303d80 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -20,9 +20,6 @@ def make_custom_fields(): insert_after='group_same_items', print_hide=1, collapsible=1), dict(fieldname='permit_no', label='Permit Number', fieldtype='Data', insert_after='vat_section', print_hide=1), - dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='permit_no', print_hide=1, - options='Y\nN', default='N') ] purchase_invoice_fields = [ @@ -31,7 +28,12 @@ def make_custom_fields(): fetch_from='company.tax_id', print_hide=1), dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Read Only', insert_after='supplier_name', - fetch_from='supplier.supplier_name_in_arabic', print_hide=1) + fetch_from='supplier.supplier_name_in_arabic', print_hide=1), + dict(fieldname='reverse_charge', label='Reverse Charge Applicable', + fieldtype='Select', insert_after='permit_no', print_hide=1, + options='Y\nN', default='N'), + dict(fieldname='claimable_reverse_charge', label='Claimable Reverse Charge (Percentage)', + insert_after='reverse_charge', fieldtype='Percent'), ] sales_invoice_fields = [ @@ -43,6 +45,12 @@ def make_custom_fields(): fetch_from='customer.customer_name_in_arabic', print_hide=1), dict(fieldname='emirate', label='Emirate', insert_after='customer_address', fieldtype='Read Only', fetch_from='customer_address.emirates'), + dict(fieldname='returns_column_break', fieldtype='Column Break', + insert_after='select_print_heading'), + dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', + insert_after='returns_column_break', fieldtype='Currency',), + dict(fieldname='standard_rated_expenses', label='Standard Rated Expenses (AED)', + insert_after='tourist_tax_return', fieldtype='Currency',), ] invoice_item_fields = [ @@ -69,6 +77,10 @@ def make_custom_fields(): 'Item': [ dict(fieldname='tax_code', label='Tax Code', fieldtype='Data', insert_after='item_group'), + # dict(fieldname='is_zero_rated', label='Is Zero Rated', + # fieldtype='Check', insert_after='tax_code'), + # dict(fieldname='is_exempt', label='Is Exempt ', + # fieldtype='Check', insert_after='is_zero_rated') ], 'Customer': [ dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 05a99d190a..5242c63e63 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -166,4 +166,14 @@ def make_regional_gl_entries(gl_entries, doc): else tax.tax_amount_after_discount_amount }, account_currency, item=tax) ) - return gl_entries \ No newline at end of file + return gl_entries + +def validate_returns(doc, method): + print("validate_returns") + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'United Arab Emirates': + return + + if flt(doc.tourist_tax_return) + flt(doc.standard_rated_expenses) > flt(doc.total_taxes_and_charges): + frappe.throw(_("The Total Returns(Tax Refund provided to Tourists (AED) + Standard Rated Expenses (AED)) should be less than the Total Taxes and Charges (Company Currency)")) \ No newline at end of file From 5225215d3116cefd45f5c1ed7a7fce2fddab9522 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 24 Sep 2020 18:41:43 +0530 Subject: [PATCH 008/229] feat(UAE VAT 21): Add rows for inputs and tourists --- erpnext/regional/report/uae_vat/uae_vat.py | 168 +++++++++++++++++- .../regional/united_arab_emirates/setup.py | 11 +- 2 files changed, 173 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat/uae_vat.py index 8eeff51723..b3e52be3e5 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.py +++ b/erpnext/regional/report/uae_vat/uae_vat.py @@ -61,6 +61,7 @@ def get_data(filters = None): Dict: Dictionary containing chart data """ data = [] + data.append({"legend": f'VAT on Sales and All Other Outputs',}) total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() amounts_by_emirate = {} @@ -88,6 +89,16 @@ def get_data(filters = None): "vat_amount": 0 } ) + + data.append( + { + "no": '2', + "legend": f'Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme', + "amount": (-1) * get_tourist_tax_return_total(filters), + "vat_amount": (-1) * get_tourist_tax_return_tax(filters) + } + ) + data.append( { "no": '3', @@ -96,6 +107,25 @@ def get_data(filters = None): "vat_amount": get_reverse_charge_tax(filters) } ) + + data.append({"legend": f'VAT on Expenses and All Other Inputs'}) + data.append( + { + "no": '9', + "legend": f'Standard Rated Expenses', + "amount": get_standard_rated_expenses_total(filters), + "vat_amount": get_standard_rated_expenses_tax(filters) + } + ) + data.append( + { + "no": '10', + "legend": f'Supplies subject to the reverse charge provision', + "amount": get_reverse_charge_recoverable_total(filters), + "vat_amount": get_reverse_charge_recoverable_tax(filters) + } + ) + return data, chart @@ -235,4 +265,140 @@ def get_conditions_join(filters): ("to_date", f' and `tabPurchase Invoice`.posting_date<="{filters.get("to_date")}"')): if filters.get(opts[0]): conditions += opts[1] - return conditions \ No newline at end of file + return conditions + + +def get_reverse_charge_recoverable_total(filters): + """Returns the sum of the total of each Purchase invoice made with claimable reverse charge + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the total of each Purchase invoice made with claimable reverse charge + """ + conditions = """ + for opts in (("company", f' and company="{filters.get("company")}"'), + ("from_date", f' and posting_date>="{filters.get("from_date")}"'), + ("to_date", f' and posting_date<="{filters.get("to_date")}"')): + if filters.get(opts[0]): + conditions += opts[1] + return conditions + """ + return frappe.db.sql(f""" + select sum(total) from + `tabPurchase Invoice` + where + reverse_charge = "Y" + and claimable_reverse_charge > 0 + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] + + +def get_reverse_charge_recoverable_tax(filters): + """Returns the sum of the tax of each Purchase invoice made + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the tax of each Purchase invoice made + """ + return frappe.db.sql(f""" + select sum(debit * `tabPurchase Invoice`.claimable_reverse_charge / 100) from + `tabPurchase Invoice` inner join `tabGL Entry` + on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name + where + `tabPurchase Invoice`.reverse_charge = "Y" + and `tabPurchase Invoice`.docstatus = 1 + and `tabPurchase Invoice`.claimable_reverse_charge > 0 + and `tabGL Entry`.docstatus = 1 {get_conditions_join(filters)} + and account in ("{'", "'.join(get_tax_accounts(filters['company']))}"); + """)[0][0] + + +def get_standard_rated_expenses_total(filters): + """Returns the sum of the total of each Purchase invoice made with claimable reverse charge + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the total of each Purchase invoice made with claimable reverse charge + """ + conditions = """ + for opts in (("company", f' and company="{filters.get("company")}"'), + ("from_date", f' and posting_date>="{filters.get("from_date")}"'), + ("to_date", f' and posting_date<="{filters.get("to_date")}"')): + if filters.get(opts[0]): + conditions += opts[1] + return conditions + """ + return frappe.db.sql(f""" + select sum(total) from + `tabSales Invoice` + where + standard_rated_expenses > 0 + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] + + +def get_standard_rated_expenses_tax(filters): + """Returns the sum of the tax of each Purchase invoice made + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the tax of each Purchase invoice made + """ + return frappe.db.sql(f""" + select sum(standard_rated_expenses) from + `tabSales Invoice` + where + standard_rated_expenses > 0 + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] + +def get_tourist_tax_return_total(filters): + """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the total of each Sales invoice with non zero tourist_tax_return + """ + conditions = """ + for opts in (("company", f' and company="{filters.get("company")}"'), + ("from_date", f' and posting_date>="{filters.get("from_date")}"'), + ("to_date", f' and posting_date<="{filters.get("to_date")}"')): + if filters.get(opts[0]): + conditions += opts[1] + return conditions + """ + return frappe.db.sql(f""" + select sum(total) from + `tabSales Invoice` + where + tourist_tax_return > 0 + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] + + +def get_tourist_tax_return_tax(filters): + """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of the tax of each Sales invoice with non zero tourist_tax_return + """ + return frappe.db.sql(f""" + select sum(tourist_tax_return) from + `tabSales Invoice` + where + tourist_tax_return > 0 + and docstatus = 1 {get_conditions(filters)} ; + """)[0][0] \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index f116303d80..01763402b1 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -33,7 +33,8 @@ def make_custom_fields(): fieldtype='Select', insert_after='permit_no', print_hide=1, options='Y\nN', default='N'), dict(fieldname='claimable_reverse_charge', label='Claimable Reverse Charge (Percentage)', - insert_after='reverse_charge', fieldtype='Percent'), + insert_after='reverse_charge', fieldtype='Percent', print_hide=1, + depends_on="eval:doc.reverse_charge=='Y'", default='100.000'), ] sales_invoice_fields = [ @@ -45,12 +46,12 @@ def make_custom_fields(): fetch_from='customer.customer_name_in_arabic', print_hide=1), dict(fieldname='emirate', label='Emirate', insert_after='customer_address', fieldtype='Read Only', fetch_from='customer_address.emirates'), - dict(fieldname='returns_column_break', fieldtype='Column Break', - insert_after='select_print_heading'), + # dict(fieldname='returns_column_break', fieldtype='Column Break', + # insert_after='select_print_heading'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', - insert_after='returns_column_break', fieldtype='Currency',), + insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), dict(fieldname='standard_rated_expenses', label='Standard Rated Expenses (AED)', - insert_after='tourist_tax_return', fieldtype='Currency',), + insert_after='tourist_tax_return', fieldtype='Currency', print_hide=1, default='0'), ] invoice_item_fields = [ From 72cad2a8abd173b2c84029bfdff6df38d6e87a6f Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Fri, 25 Sep 2020 13:13:38 +0530 Subject: [PATCH 009/229] feat(UAE VAT 21): Add zero rated and vat exempt --- erpnext/regional/report/uae_vat/uae_vat.js | 12 ++- erpnext/regional/report/uae_vat/uae_vat.py | 87 +++++++++++++++---- .../regional/united_arab_emirates/setup.py | 21 +++-- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/erpnext/regional/report/uae_vat/uae_vat.js b/erpnext/regional/report/uae_vat/uae_vat.js index 45df167823..0213956d44 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.js +++ b/erpnext/regional/report/uae_vat/uae_vat.js @@ -27,5 +27,15 @@ frappe.query_reports["UAE VAT"] = { "reqd": 1, "default": frappe.datetime.get_today() }, - ] + ], + "formatter": function(value, row, column, data, default_formatter) { + if (data + && (data.legend=='VAT on Sales and All Other Outputs' || data.legend=='VAT on Expenses and All Other Inputs') + && data.legend==value) { + value = $(`${value}`); + var $value = $(value).css("font-weight", "bold"); + value = $value.wrap("

").parent().html(); + } + return value; + }, }; diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat/uae_vat.py index b3e52be3e5..1a3537c268 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.py +++ b/erpnext/regional/report/uae_vat/uae_vat.py @@ -41,12 +41,6 @@ def get_columns(): "label": "VAT Amount (AED)", "fieldtype": "Currency", "width": 100 - }, - { - "fieldname": "adjustment", - "label": "Adjustment (AED)", - "fieldtype": "Currency", - "width": 100 } ] @@ -61,7 +55,12 @@ def get_data(filters = None): Dict: Dictionary containing chart data """ data = [] - data.append({"legend": f'VAT on Sales and All Other Outputs',}) + data.append({ + "no": '', + "legend": f'VAT on Sales and All Other Outputs', + "amount": '', + "vat_amount": '' + }) total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() amounts_by_emirate = {} @@ -108,7 +107,30 @@ def get_data(filters = None): } ) - data.append({"legend": f'VAT on Expenses and All Other Inputs'}) + data.append( + { + "no": '4', + "legend": f'Zero Rated', + "amount": get_zero_rated_total(filters), + "vat_amount": "-" + } + ) + + data.append( + { + "no": '5', + "legend": f'Exempt Supplies', + "amount": get_exempt_total(filters), + "vat_amount": "-" + } + ) + + data.append({ + "no": '', + "legend": f'VAT on Expenses and All Other Inputs', + "amount": '', + "vat_amount": '' + }) data.append( { "no": '9', @@ -226,7 +248,7 @@ def get_reverse_charge_total(filters): where reverse_charge = "Y" and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] + """)[0][0] or 0 def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made @@ -246,7 +268,7 @@ def get_reverse_charge_tax(filters): and `tabPurchase Invoice`.docstatus = 1 and `tabGL Entry`.docstatus = 1 {get_conditions_join(filters)} and account in ("{'", "'.join(get_tax_accounts(filters['company']))}"); - """)[0][0] + """)[0][0] or 0 @@ -292,7 +314,7 @@ def get_reverse_charge_recoverable_total(filters): reverse_charge = "Y" and claimable_reverse_charge > 0 and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] + """)[0][0] or 0 def get_reverse_charge_recoverable_tax(filters): @@ -314,7 +336,7 @@ def get_reverse_charge_recoverable_tax(filters): and `tabPurchase Invoice`.claimable_reverse_charge > 0 and `tabGL Entry`.docstatus = 1 {get_conditions_join(filters)} and account in ("{'", "'.join(get_tax_accounts(filters['company']))}"); - """)[0][0] + """)[0][0] or 0 def get_standard_rated_expenses_total(filters): @@ -340,7 +362,7 @@ def get_standard_rated_expenses_total(filters): where standard_rated_expenses > 0 and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] + """)[0][0] or 0 def get_standard_rated_expenses_tax(filters): @@ -358,7 +380,7 @@ def get_standard_rated_expenses_tax(filters): where standard_rated_expenses > 0 and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] + """)[0][0] or 0 def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return @@ -383,7 +405,7 @@ def get_tourist_tax_return_total(filters): where tourist_tax_return > 0 and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] + """)[0][0] or 0 def get_tourist_tax_return_tax(filters): @@ -401,4 +423,37 @@ def get_tourist_tax_return_tax(filters): where tourist_tax_return > 0 and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] \ No newline at end of file + """)[0][0] or 0 + +def get_zero_rated_total(filters): + """Returns the sum of each Sales Invoice Item Amount which is zero rated + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of each Sales Invoice Item Amount which is zero rated + """ + return frappe.db.sql(f""" + select sum(i.base_amount) as total from + `tabSales Invoice Item` i, `tabSales Invoice` s + where s.docstatus = 1 and i.parent = s.name and i.is_zero_rated = 1 + {get_conditions(filters)} ; + """)[0][0] or 0 + + +def get_exempt_total(filters): + """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt + + Args: + filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. + + Returns: + Float: sum of each Sales Invoice Item Amount which is Vat Exempt + """ + return frappe.db.sql(f""" + select sum(i.base_amount) as total from + `tabSales Invoice Item` i, `tabSales Invoice` s + where s.docstatus = 1 and i.parent = s.name and i.is_exempt = 1 + {get_conditions(filters)} ; + """)[0][0] or 0 \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 01763402b1..29ad41c9af 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -15,6 +15,13 @@ def setup(company=None, patch=True): create_sales_tax(company) def make_custom_fields(): + is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', + print_hide=1) + is_exempt = dict(fieldname='is_exempt', label='Is Exempt', + fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated', + print_hide=1) + invoice_fields = [ dict(fieldname='vat_section', label='VAT Details', fieldtype='Section Break', insert_after='group_same_items', print_hide=1, collapsible=1), @@ -46,8 +53,6 @@ def make_custom_fields(): fetch_from='customer.customer_name_in_arabic', print_hide=1), dict(fieldname='emirate', label='Emirate', insert_after='customer_address', fieldtype='Read Only', fetch_from='customer_address.emirates'), - # dict(fieldname='returns_column_break', fieldtype='Column Break', - # insert_after='select_print_heading'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), dict(fieldname='standard_rated_expenses', label='Standard Rated Expenses (AED)', @@ -78,10 +83,12 @@ def make_custom_fields(): 'Item': [ dict(fieldname='tax_code', label='Tax Code', fieldtype='Data', insert_after='item_group'), - # dict(fieldname='is_zero_rated', label='Is Zero Rated', - # fieldtype='Check', insert_after='tax_code'), - # dict(fieldname='is_exempt', label='Is Exempt ', - # fieldtype='Check', insert_after='is_zero_rated') + dict(fieldname='is_zero_rated', label='Is Zero Rated', + fieldtype='Check', insert_after='tax_code', + print_hide=1), + dict(fieldname='is_exempt', label='Is Exempt ', + fieldtype='Check', insert_after='is_zero_rated', + print_hide=1) ], 'Customer': [ dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', @@ -101,7 +108,7 @@ def make_custom_fields(): 'Sales Invoice': sales_invoice_fields + invoice_fields, 'Sales Order': sales_invoice_fields + invoice_fields, 'Delivery Note': sales_invoice_fields + invoice_fields, - 'Sales Invoice Item': invoice_item_fields + delivery_date_field, + 'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], 'Purchase Invoice Item': invoice_item_fields, 'Sales Order Item': invoice_item_fields, 'Delivery Note Item': invoice_item_fields, From 321b5f54aef19c66460657552695af65b4a977a6 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Fri, 25 Sep 2020 14:24:57 +0530 Subject: [PATCH 010/229] feat(UAE VAT 21): Add Roles for UAE VAT 21 Report --- erpnext/regional/united_arab_emirates/setup.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 29ad41c9af..95fde3ab9f 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -10,6 +10,7 @@ from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax def setup(company=None, patch=True): make_custom_fields() add_print_formats() + add_custom_roles_for_reports() if company: create_sales_tax(company) @@ -127,3 +128,15 @@ def add_print_formats(): frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) + +def add_custom_roles_for_reports(): + if not frappe.db.get_value('Custom Role', dict(report='UAE VAT')): + frappe.get_doc(dict( + doctype='Custom Role', + report='UAE VAT', + roles= [ + dict(role='Accounts User'), + dict(role='Accounts Manager'), + dict(role='Auditor') + ] + )).insert() \ No newline at end of file From 127fbfcb931f4a7ed215a6102dfa3e8ff18155f7 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Fri, 25 Sep 2020 20:42:54 +0530 Subject: [PATCH 011/229] feat(UAE VAT 21): Added Print Format --- erpnext/regional/report/uae_vat/uae_vat.html | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 erpnext/regional/report/uae_vat/uae_vat.html diff --git a/erpnext/regional/report/uae_vat/uae_vat.html b/erpnext/regional/report/uae_vat/uae_vat.html new file mode 100644 index 0000000000..861c0c818d --- /dev/null +++ b/erpnext/regional/report/uae_vat/uae_vat.html @@ -0,0 +1,68 @@ +{% + var report_columns = report.get_columns_for_print(); + report_columns = report_columns.filter(col => !col.hidden); + + if (report_columns.length > 8) { + frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application.")); + } +%} + +

{%= __(report.report_name) %}

+ +

{%= __("VAT on Sales and All Other Outputs") %}

+ + + + + {% for (let i=0; i{%= report_columns[i].label %} + {% } %} + + + + {% for (let j=1; j<12; j++) { %} + {% + var row = data[j]; + %} + + {% for (let i=0; i + {% const fieldname = report_columns[i].fieldname; %} + {% if (!is_null(row[fieldname])) { %} + {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} + {% } %} + + {% } %} + + {% } %} + +
+ +

{%= __("VAT on Expenses and All Other Inputs") %}

+ + + + {% for (let i=0; i{%= report_columns[i].label %} + {% } %} + + + + {% for (let j=12; j + {% for (let i=0; i + {% const fieldname = report_columns[i].fieldname; %} + {% if (!is_null(row[fieldname])) { %} + {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} + {% } %} + + {% } %} + + {% } %} + + +
\ No newline at end of file From 79aca28453c2eb5a7d8c917459269c2efcb0a96d Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 12:27:41 +0530 Subject: [PATCH 012/229] refactor(UAE VAT 21): rename UAE VAT - UAE VAT 21 --- erpnext/regional/report/{uae_vat => uae_vat_21}/__init__.py | 0 .../{uae_vat/uae_vat.html => uae_vat_21/uae_vat_21.html} | 0 .../report/{uae_vat/uae_vat.js => uae_vat_21/uae_vat_21.js} | 2 +- .../{uae_vat/uae_vat.json => uae_vat_21/uae_vat_21.json} | 4 ++-- .../report/{uae_vat/uae_vat.py => uae_vat_21/uae_vat_21.py} | 0 erpnext/regional/united_arab_emirates/setup.py | 4 ++-- 6 files changed, 5 insertions(+), 5 deletions(-) rename erpnext/regional/report/{uae_vat => uae_vat_21}/__init__.py (100%) rename erpnext/regional/report/{uae_vat/uae_vat.html => uae_vat_21/uae_vat_21.html} (100%) rename erpnext/regional/report/{uae_vat/uae_vat.js => uae_vat_21/uae_vat_21.js} (96%) rename erpnext/regional/report/{uae_vat/uae_vat.json => uae_vat_21/uae_vat_21.json} (89%) rename erpnext/regional/report/{uae_vat/uae_vat.py => uae_vat_21/uae_vat_21.py} (100%) diff --git a/erpnext/regional/report/uae_vat/__init__.py b/erpnext/regional/report/uae_vat_21/__init__.py similarity index 100% rename from erpnext/regional/report/uae_vat/__init__.py rename to erpnext/regional/report/uae_vat_21/__init__.py diff --git a/erpnext/regional/report/uae_vat/uae_vat.html b/erpnext/regional/report/uae_vat_21/uae_vat_21.html similarity index 100% rename from erpnext/regional/report/uae_vat/uae_vat.html rename to erpnext/regional/report/uae_vat_21/uae_vat_21.html diff --git a/erpnext/regional/report/uae_vat/uae_vat.js b/erpnext/regional/report/uae_vat_21/uae_vat_21.js similarity index 96% rename from erpnext/regional/report/uae_vat/uae_vat.js rename to erpnext/regional/report/uae_vat_21/uae_vat_21.js index 0213956d44..3e6cdd85e5 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.js +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.js @@ -2,7 +2,7 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.query_reports["UAE VAT"] = { +frappe.query_reports["UAE VAT 21"] = { "filters": [ { "fieldname": "company", diff --git a/erpnext/regional/report/uae_vat/uae_vat.json b/erpnext/regional/report/uae_vat_21/uae_vat_21.json similarity index 89% rename from erpnext/regional/report/uae_vat/uae_vat.json rename to erpnext/regional/report/uae_vat_21/uae_vat_21.json index 8807405a98..421990cc4a 100644 --- a/erpnext/regional/report/uae_vat/uae_vat.json +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.json @@ -12,11 +12,11 @@ "modified": "2020-09-10 08:51:02.298482", "modified_by": "Administrator", "module": "Regional", - "name": "UAE VAT", + "name": "UAE VAT 21", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "GL Entry", - "report_name": "UAE VAT", + "report_name": "UAE VAT 21", "report_type": "Script Report", "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/uae_vat/uae_vat.py b/erpnext/regional/report/uae_vat_21/uae_vat_21.py similarity index 100% rename from erpnext/regional/report/uae_vat/uae_vat.py rename to erpnext/regional/report/uae_vat_21/uae_vat_21.py diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 95fde3ab9f..e7a0609f4a 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -130,10 +130,10 @@ def add_print_formats(): name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) def add_custom_roles_for_reports(): - if not frappe.db.get_value('Custom Role', dict(report='UAE VAT')): + if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 21')): frappe.get_doc(dict( doctype='Custom Role', - report='UAE VAT', + report='UAE VAT 21', roles= [ dict(role='Accounts User'), dict(role='Accounts Manager'), From 815b7584dddbe4c29cc453c98d80d697035103c7 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 13:24:40 +0530 Subject: [PATCH 013/229] refactor(UAE VAT 21): seperate get_chart, get_data --- erpnext/regional/report/uae_vat_21/uae_vat_21.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.py b/erpnext/regional/report/uae_vat_21/uae_vat_21.py index 1a3537c268..d21cecd826 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.py +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.py @@ -8,7 +8,9 @@ from frappe import _ def execute(filters=None): columns = get_columns() - data, chart = get_data(filters) + data, emirates, amounts_by_emirate = get_data(filters) + chart = get_chart(emirates, amounts_by_emirate) + return columns, data, None, chart def get_columns(): @@ -72,8 +74,6 @@ def get_data(filters = None): "vat_amount": vat } - chart = get_chart_data(emirates, amounts_by_emirate) - for d, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: amounts_by_emirate[emirate]["no"] = f'1{chr(d)}' @@ -148,10 +148,10 @@ def get_data(filters = None): } ) - return data, chart + return data, emirates, amounts_by_emirate -def get_chart_data(emirates, amounts_by_emirate): +def get_chart(emirates, amounts_by_emirate): """Returns chart data Args: From bed1add349cf571a912f8956ba7d86b91f48cfbe Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 13:26:05 +0530 Subject: [PATCH 014/229] docs(UAE VAT 21): add docstrings --- erpnext/regional/united_arab_emirates/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index e7a0609f4a..07543b31ea 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -130,6 +130,8 @@ def add_print_formats(): name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) def add_custom_roles_for_reports(): + """Add Access Control to UAE VAT 21 + """ if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 21')): frappe.get_doc(dict( doctype='Custom Role', From 2a601e59bfb78d74551fb02af82fb17d8acef725 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 13:26:43 +0530 Subject: [PATCH 015/229] fix(UAE VAT 21): remove extra legend --- erpnext/regional/report/uae_vat_21/uae_vat_21.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.html b/erpnext/regional/report/uae_vat_21/uae_vat_21.html index 861c0c818d..aa6a255932 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.html +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.html @@ -48,7 +48,7 @@ - {% for (let j=12; j Date: Mon, 28 Sep 2020 14:38:14 +0530 Subject: [PATCH 016/229] feat(UAE VAT 21): Add vat settings --- .../doctype/uae_vat_account/__init__.py | 0 .../uae_vat_account/uae_vat_account.json | 35 ++++++++++++ .../uae_vat_account/uae_vat_account.py | 10 ++++ .../doctype/uae_vat_setting/__init__.py | 0 .../uae_vat_setting/test_uae_vat_setting.py | 10 ++++ .../uae_vat_setting/uae_vat_setting.js | 8 +++ .../uae_vat_setting/uae_vat_setting.json | 55 +++++++++++++++++++ .../uae_vat_setting/uae_vat_setting.py | 10 ++++ .../regional/united_arab_emirates/utils.py | 9 +-- 9 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 erpnext/regional/doctype/uae_vat_account/__init__.py create mode 100644 erpnext/regional/doctype/uae_vat_account/uae_vat_account.json create mode 100644 erpnext/regional/doctype/uae_vat_account/uae_vat_account.py create mode 100644 erpnext/regional/doctype/uae_vat_setting/__init__.py create mode 100644 erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py create mode 100644 erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js create mode 100644 erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json create mode 100644 erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py diff --git a/erpnext/regional/doctype/uae_vat_account/__init__.py b/erpnext/regional/doctype/uae_vat_account/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/uae_vat_account/uae_vat_account.json b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.json new file mode 100644 index 0000000000..73a8169207 --- /dev/null +++ b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "autoname": "account", + "creation": "2020-09-28 11:30:45.472053", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "label": "Account", + "options": "Account" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-28 12:02:56.444007", + "modified_by": "Administrator", + "module": "Regional", + "name": "UAE VAT Account", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/uae_vat_account/uae_vat_account.py b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.py new file mode 100644 index 0000000000..80d6b3a5f1 --- /dev/null +++ b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UAEVATAccount(Document): + pass diff --git a/erpnext/regional/doctype/uae_vat_setting/__init__.py b/erpnext/regional/doctype/uae_vat_setting/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py b/erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py new file mode 100644 index 0000000000..a5f151dc73 --- /dev/null +++ b/erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestUAEVATSetting(unittest.TestCase): + pass diff --git a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js b/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js new file mode 100644 index 0000000000..f910e1f662 --- /dev/null +++ b/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('UAE VAT Setting', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json b/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json new file mode 100644 index 0000000000..e2e2ab8245 --- /dev/null +++ b/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "autoname": "field:company", + "creation": "2020-09-25 12:48:51.463265", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "uae_vat_account" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "uae_vat_account", + "fieldtype": "Table", + "label": "UAE VAT Account", + "options": "UAE VAT Account", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-28 12:19:11.493138", + "modified_by": "Administrator", + "module": "Regional", + "name": "UAE VAT Setting", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py b/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py new file mode 100644 index 0000000000..9549de9466 --- /dev/null +++ b/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UAEVATSetting(Document): + pass diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 5242c63e63..4424d2efec 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -53,12 +53,13 @@ def get_tax_accounts(company): tax_accounts: List of Tax Accounts for the company """ tax_accounts_dict = frappe._dict() - tax_accounts_list = frappe.get_all("Account", - filters={"account_type": "Tax", "company": company}, - fields=["name"]) + tax_accounts_list = frappe.get_all("UAE VAT Account", + filters={"parent": company}, + fields=["Account"] + ) if not tax_accounts_list and not frappe.flags.in_test: - frappe.throw(_("Please create at least one Account of type Tax")) + frappe.throw(_(f'Please set Vat Accounts for Company: "{company}" in UAE VAT Setting')) for d in tax_accounts_list: for key, name in d.items(): tax_accounts_dict[name] = name From 0860c74c824e337a2e5e689d67614a2644e663b8 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 14:38:45 +0530 Subject: [PATCH 017/229] feat(UAE VAT 21): Add permissions for doctypes --- erpnext/regional/united_arab_emirates/setup.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 07543b31ea..1c947d2fb0 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -5,13 +5,14 @@ from __future__ import unicode_literals import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.permissions import add_permission, update_permission_property from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax def setup(company=None, patch=True): make_custom_fields() add_print_formats() add_custom_roles_for_reports() - + add_permissions() if company: create_sales_tax(company) @@ -141,4 +142,14 @@ def add_custom_roles_for_reports(): dict(role='Accounts Manager'), dict(role='Auditor') ] - )).insert() \ No newline at end of file + )).insert() + +def add_permissions(): + """Add Permissions for UAE VAT Settings and UAE VAT Account + """ + for doctype in ('UAE VAT Setting', 'UAE VAT Account'): + add_permission(doctype, 'All', 0) + for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + add_permission(doctype, role, 0) + update_permission_property(doctype, role, 0, 'write', 1) + update_permission_property(doctype, role, 0, 'create', 1) From 587bb05a37a068962620526c47450731ee09d83f Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 14:52:29 +0530 Subject: [PATCH 018/229] feat(UAE VAT 21): Add desk page for UAE VAT 21 --- erpnext/accounts/desk_page/accounting/accounting.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 2c5231491c..6621e8e0ce 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -79,6 +79,11 @@ "hidden": 0, "label": "Profitability", "links": "[\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Gross Profit\",\n \"name\": \"Gross Profit\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profitability Analysis\",\n \"name\": \"Profitability Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"type\": \"report\"\n }\n]" + }, + { + "hidden": 0, + "label": "Value-Added Tax (VAT UAE)", + "links": "[\n {\n \"label\": \"UAE VAT Setting\",\n \"name\": \"UAE VAT Setting\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"UAE VAT 21\",\n \"name\": \"UAE VAT 21\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -98,7 +103,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-09-03 10:37:07.865801", + "modified": "2020-09-28 13:19:27.703992", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", From 80068e16e856ebd37c782132be1d68c0c9d2e923 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 18:58:27 +0530 Subject: [PATCH 019/229] Refactor(UAE VAT 21): Replace f-strings by format --- .../regional/report/uae_vat_21/uae_vat_21.py | 135 ++++++++---------- 1 file changed, 57 insertions(+), 78 deletions(-) diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.py b/erpnext/regional/report/uae_vat_21/uae_vat_21.py index d21cecd826..6cb39e0835 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.py +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.py @@ -218,9 +218,9 @@ def get_conditions(filters): String: Concatenated list of conditions to be applied to calculate the total sale """ conditions = "" - for opts in (("company", f' and company="{filters.get("company")}"'), - ("from_date", f' and posting_date>="{filters.get("from_date")}"'), - ("to_date", f' and posting_date<="{filters.get("to_date")}"')): + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): if filters.get(opts[0]): conditions += opts[1] return conditions @@ -234,21 +234,21 @@ def get_reverse_charge_total(filters): Returns: Float: sum of the total of each Purchase invoice made """ - conditions = """ - for opts in (("company", f' and company="{filters.get("company")}"'), - ("from_date", f' and posting_date>="{filters.get("from_date")}"'), - ("to_date", f' and posting_date<="{filters.get("to_date")}"')): - if filters.get(opts[0]): - conditions += opts[1] - return conditions - """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + print(""" select sum(total) from `tabPurchase Invoice` where reverse_charge = "Y" - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] or 0 + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions)) + return frappe.db.sql(""" + select sum(total) from + `tabPurchase Invoice` + where + reverse_charge = "Y" + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made @@ -259,18 +259,18 @@ def get_reverse_charge_tax(filters): Returns: Float: sum of the tax of each Purchase invoice made """ - return frappe.db.sql(f""" + conditions = get_conditions_join(filters) + return frappe.db.sql(""" select sum(debit) from `tabPurchase Invoice` inner join `tabGL Entry` on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name where `tabPurchase Invoice`.reverse_charge = "Y" and `tabPurchase Invoice`.docstatus = 1 - and `tabGL Entry`.docstatus = 1 {get_conditions_join(filters)} - and account in ("{'", "'.join(get_tax_accounts(filters['company']))}"); - """)[0][0] or 0 - - + and `tabGL Entry`.docstatus = 1 + and account in (select account from `tabUAE VAT Account` where parent=%(company)s) + {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_conditions_join(filters): """The conditions to be used to filter data to calculate the total vat @@ -282,14 +282,13 @@ def get_conditions_join(filters): String: Concatenated list of conditions to be applied to calculate the total vat """ conditions = "" - for opts in (("company", f' and `tabPurchase Invoice`.company="{filters.get("company")}"'), - ("from_date", f' and `tabPurchase Invoice`.posting_date>="{filters.get("from_date")}"'), - ("to_date", f' and `tabPurchase Invoice`.posting_date<="{filters.get("to_date")}"')): + for opts in (("company", " and `tabPurchase Invoice`.company=%(company)s"), + ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), + ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s")): if filters.get(opts[0]): conditions += opts[1] return conditions - def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with claimable reverse charge @@ -299,23 +298,15 @@ def get_reverse_charge_recoverable_total(filters): Returns: Float: sum of the total of each Purchase invoice made with claimable reverse charge """ - conditions = """ - for opts in (("company", f' and company="{filters.get("company")}"'), - ("from_date", f' and posting_date>="{filters.get("from_date")}"'), - ("to_date", f' and posting_date<="{filters.get("to_date")}"')): - if filters.get(opts[0]): - conditions += opts[1] - return conditions - """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(total) from `tabPurchase Invoice` where reverse_charge = "Y" and claimable_reverse_charge > 0 - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] or 0 - + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made @@ -326,7 +317,8 @@ def get_reverse_charge_recoverable_tax(filters): Returns: Float: sum of the tax of each Purchase invoice made """ - return frappe.db.sql(f""" + conditions = get_conditions_join(filters) + return frappe.db.sql(""" select sum(debit * `tabPurchase Invoice`.claimable_reverse_charge / 100) from `tabPurchase Invoice` inner join `tabGL Entry` on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name @@ -334,10 +326,10 @@ def get_reverse_charge_recoverable_tax(filters): `tabPurchase Invoice`.reverse_charge = "Y" and `tabPurchase Invoice`.docstatus = 1 and `tabPurchase Invoice`.claimable_reverse_charge > 0 - and `tabGL Entry`.docstatus = 1 {get_conditions_join(filters)} - and account in ("{'", "'.join(get_tax_accounts(filters['company']))}"); - """)[0][0] or 0 - + and `tabGL Entry`.docstatus = 1 + and account in (select account from `tabUAE VAT Account` where parent=%(company)s) + {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with claimable reverse charge @@ -348,22 +340,14 @@ def get_standard_rated_expenses_total(filters): Returns: Float: sum of the total of each Purchase invoice made with claimable reverse charge """ - conditions = """ - for opts in (("company", f' and company="{filters.get("company")}"'), - ("from_date", f' and posting_date>="{filters.get("from_date")}"'), - ("to_date", f' and posting_date<="{filters.get("to_date")}"')): - if filters.get(opts[0]): - conditions += opts[1] - return conditions - """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(total) from `tabSales Invoice` where standard_rated_expenses > 0 - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] or 0 - + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made @@ -374,13 +358,14 @@ def get_standard_rated_expenses_tax(filters): Returns: Float: sum of the tax of each Purchase invoice made """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(standard_rated_expenses) from `tabSales Invoice` where standard_rated_expenses > 0 - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] or 0 + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return @@ -391,22 +376,14 @@ def get_tourist_tax_return_total(filters): Returns: Float: sum of the total of each Sales invoice with non zero tourist_tax_return """ - conditions = """ - for opts in (("company", f' and company="{filters.get("company")}"'), - ("from_date", f' and posting_date>="{filters.get("from_date")}"'), - ("to_date", f' and posting_date<="{filters.get("to_date")}"')): - if filters.get(opts[0]): - conditions += opts[1] - return conditions - """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(total) from `tabSales Invoice` where tourist_tax_return > 0 - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] or 0 - + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return @@ -417,13 +394,14 @@ def get_tourist_tax_return_tax(filters): Returns: Float: sum of the tax of each Sales invoice with non zero tourist_tax_return """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(tourist_tax_return) from `tabSales Invoice` where tourist_tax_return > 0 - and docstatus = 1 {get_conditions(filters)} ; - """)[0][0] or 0 + and docstatus = 1 {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated @@ -434,13 +412,13 @@ def get_zero_rated_total(filters): Returns: Float: sum of each Sales Invoice Item Amount which is zero rated """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(i.base_amount) as total from `tabSales Invoice Item` i, `tabSales Invoice` s where s.docstatus = 1 and i.parent = s.name and i.is_zero_rated = 1 - {get_conditions(filters)} ; - """)[0][0] or 0 - + {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 def get_exempt_total(filters): """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt @@ -451,9 +429,10 @@ def get_exempt_total(filters): Returns: Float: sum of each Sales Invoice Item Amount which is Vat Exempt """ - return frappe.db.sql(f""" + conditions = get_conditions(filters) + return frappe.db.sql(""" select sum(i.base_amount) as total from `tabSales Invoice Item` i, `tabSales Invoice` s where s.docstatus = 1 and i.parent = s.name and i.is_exempt = 1 - {get_conditions(filters)} ; - """)[0][0] or 0 \ No newline at end of file + {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 \ No newline at end of file From a5e8e449eea1266b225d1616b0b400cd51feeb01 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Mon, 28 Sep 2020 20:43:03 +0530 Subject: [PATCH 020/229] chore: Merge branch 'develop' into UAE-VAT-Format --- erpnext/accounts/desk_page/accounting/accounting.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 29d1331b5d..85635572b1 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -103,11 +103,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", -<<<<<<< HEAD "modified": "2020-09-28 13:19:27.703992", -======= - "modified": "2020-09-09 11:45:33.766400", ->>>>>>> develop "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", From 3294f86b9235d57f9f143e7cc33cf367816d4aa1 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Wed, 30 Sep 2020 12:15:07 +0530 Subject: [PATCH 021/229] refactor: fix linting --- .../regional/report/uae_vat_21/uae_vat_21.py | 168 +++--------------- .../regional/united_arab_emirates/setup.py | 6 +- .../regional/united_arab_emirates/utils.py | 39 +--- 3 files changed, 36 insertions(+), 177 deletions(-) diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.py b/erpnext/regional/report/uae_vat_21/uae_vat_21.py index 6cb39e0835..d400732f39 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.py +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import frappe -from erpnext.regional.united_arab_emirates.utils import get_tax_accounts from frappe import _ def execute(filters=None): @@ -14,11 +13,7 @@ def execute(filters=None): return columns, data, None, chart def get_columns(): - """Creates a list of dictionaries that are used to generate column headers of the data table - - Returns: - List(Dict): list of dictionaries that are used to generate column headers of the data table - """ + """Creates a list of dictionaries that are used to generate column headers of the data table.""" return [ { "fieldname": "no", @@ -47,22 +42,15 @@ def get_columns(): ] def get_data(filters = None): - """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - List(Dict): Each dictionary is a row in the datatable - Dict: Dictionary containing chart data - """ + """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data.""" data = [] data.append({ "no": '', - "legend": f'VAT on Sales and All Other Outputs', + "legend": 'VAT on Sales and All Other Outputs', "amount": '', "vat_amount": '' }) + total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() amounts_by_emirate = {} @@ -92,7 +80,7 @@ def get_data(filters = None): data.append( { "no": '2', - "legend": f'Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme', + "legend": 'Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme', "amount": (-1) * get_tourist_tax_return_total(filters), "vat_amount": (-1) * get_tourist_tax_return_tax(filters) } @@ -101,7 +89,7 @@ def get_data(filters = None): data.append( { "no": '3', - "legend": f'Supplies subject to the reverse charge provision', + "legend": 'Supplies subject to the reverse charge provision', "amount": get_reverse_charge_total(filters), "vat_amount": get_reverse_charge_tax(filters) } @@ -110,7 +98,7 @@ def get_data(filters = None): data.append( { "no": '4', - "legend": f'Zero Rated', + "legend": 'Zero Rated', "amount": get_zero_rated_total(filters), "vat_amount": "-" } @@ -119,7 +107,7 @@ def get_data(filters = None): data.append( { "no": '5', - "legend": f'Exempt Supplies', + "legend": 'Exempt Supplies', "amount": get_exempt_total(filters), "vat_amount": "-" } @@ -127,22 +115,24 @@ def get_data(filters = None): data.append({ "no": '', - "legend": f'VAT on Expenses and All Other Inputs', + "legend": 'VAT on Expenses and All Other Inputs', "amount": '', "vat_amount": '' }) + data.append( { "no": '9', - "legend": f'Standard Rated Expenses', + "legend": 'Standard Rated Expenses', "amount": get_standard_rated_expenses_total(filters), "vat_amount": get_standard_rated_expenses_tax(filters) } ) + data.append( { "no": '10', - "legend": f'Supplies subject to the reverse charge provision', + "legend": 'Supplies subject to the reverse charge provision', "amount": get_reverse_charge_recoverable_total(filters), "vat_amount": get_reverse_charge_recoverable_tax(filters) } @@ -152,15 +142,7 @@ def get_data(filters = None): def get_chart(emirates, amounts_by_emirate): - """Returns chart data - - Args: - emirates (List): List of Emirates - amounts_by_emirate (Dict): Vat and Tax amount by emirates with emirates as key - - Returns: - [Dict]: Chart Data - """ + """Returns chart data.""" labels = [] amount = [] vat_amount = [] @@ -186,6 +168,7 @@ def get_chart(emirates, amounts_by_emirate): return chart def get_total_emiratewise(filters): + """Returns Emiratewise Amount and Taxes.""" return frappe.db.sql(f""" select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` where docstatus = 1 {get_conditions(filters)} @@ -193,11 +176,7 @@ def get_total_emiratewise(filters): """, filters) def get_emirates(): - """Returns a List of emirates in the order that they are to be displayed - - Returns: - List(String): List of emirates in the order that they are to be displayed - """ + """Returns a List of emirates in the order that they are to be displayed.""" return [ 'Abu Dhabi', 'Dubai', @@ -209,14 +188,7 @@ def get_emirates(): ] def get_conditions(filters): - """The conditions to be used to filter data to calculate the total sale - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - String: Concatenated list of conditions to be applied to calculate the total sale - """ + """The conditions to be used to filter data to calculate the total sale.""" conditions = "" for opts in (("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), @@ -226,22 +198,8 @@ def get_conditions(filters): return conditions def get_reverse_charge_total(filters): - """Returns the sum of the total of each Purchase invoice made - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the total of each Purchase invoice made - """ + """Returns the sum of the total of each Purchase invoice made.""" conditions = get_conditions(filters) - print(""" - select sum(total) from - `tabPurchase Invoice` - where - reverse_charge = "Y" - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions)) return frappe.db.sql(""" select sum(total) from `tabPurchase Invoice` @@ -251,14 +209,7 @@ def get_reverse_charge_total(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_reverse_charge_tax(filters): - """Returns the sum of the tax of each Purchase invoice made - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the tax of each Purchase invoice made - """ + """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" select sum(debit) from @@ -273,14 +224,7 @@ def get_reverse_charge_tax(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_conditions_join(filters): - """The conditions to be used to filter data to calculate the total vat - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - String: Concatenated list of conditions to be applied to calculate the total vat - """ + """The conditions to be used to filter data to calculate the total vat.""" conditions = "" for opts in (("company", " and `tabPurchase Invoice`.company=%(company)s"), ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), @@ -290,14 +234,7 @@ def get_conditions_join(filters): return conditions def get_reverse_charge_recoverable_total(filters): - """Returns the sum of the total of each Purchase invoice made with claimable reverse charge - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the total of each Purchase invoice made with claimable reverse charge - """ + """Returns the sum of the total of each Purchase invoice made with claimable reverse charge.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(total) from @@ -309,14 +246,7 @@ def get_reverse_charge_recoverable_total(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_reverse_charge_recoverable_tax(filters): - """Returns the sum of the tax of each Purchase invoice made - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the tax of each Purchase invoice made - """ + """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" select sum(debit * `tabPurchase Invoice`.claimable_reverse_charge / 100) from @@ -332,14 +262,7 @@ def get_reverse_charge_recoverable_tax(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_standard_rated_expenses_total(filters): - """Returns the sum of the total of each Purchase invoice made with claimable reverse charge - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the total of each Purchase invoice made with claimable reverse charge - """ + """Returns the sum of the total of each Purchase invoice made with claimable reverse charge.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(total) from @@ -350,14 +273,7 @@ def get_standard_rated_expenses_total(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_standard_rated_expenses_tax(filters): - """Returns the sum of the tax of each Purchase invoice made - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the tax of each Purchase invoice made - """ + """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(standard_rated_expenses) from @@ -368,14 +284,7 @@ def get_standard_rated_expenses_tax(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_tourist_tax_return_total(filters): - """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the total of each Sales invoice with non zero tourist_tax_return - """ + """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(total) from @@ -386,14 +295,7 @@ def get_tourist_tax_return_total(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_tourist_tax_return_tax(filters): - """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of the tax of each Sales invoice with non zero tourist_tax_return - """ + """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(tourist_tax_return) from @@ -404,14 +306,7 @@ def get_tourist_tax_return_tax(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_zero_rated_total(filters): - """Returns the sum of each Sales Invoice Item Amount which is zero rated - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of each Sales Invoice Item Amount which is zero rated - """ + """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(i.base_amount) as total from @@ -421,14 +316,7 @@ def get_zero_rated_total(filters): """.format(where_conditions=conditions), filters)[0][0] or 0 def get_exempt_total(filters): - """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt - - Args: - filters (Dict, optional): Dictionary consisting of the filters selected by the user. Defaults to None. - - Returns: - Float: sum of each Sales Invoice Item Amount which is Vat Exempt - """ + """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(i.base_amount) as total from diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 1c947d2fb0..70030c736c 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -131,8 +131,7 @@ def add_print_formats(): name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) def add_custom_roles_for_reports(): - """Add Access Control to UAE VAT 21 - """ + """Add Access Control to UAE VAT 21.""" if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 21')): frappe.get_doc(dict( doctype='Custom Role', @@ -145,8 +144,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - """Add Permissions for UAE VAT Settings and UAE VAT Account - """ + """Add Permissions for UAE VAT Settings and UAE VAT Account.""" for doctype in ('UAE VAT Setting', 'UAE VAT Account'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 4424d2efec..f75efd1aae 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -31,7 +31,7 @@ def update_itemised_tax_data(doc): row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) def get_account_currency(account): - """Helper function to get account currency""" + """Helper function to get account currency.""" if not account: return def generator(): @@ -44,14 +44,7 @@ def get_account_currency(account): return frappe.local_cache("account_currency", account, generator) def get_tax_accounts(company): - """Get the list of tax accounts for a specific company - - Args: - company (String): Current Company set as default - - Returns: - tax_accounts: List of Tax Accounts for the company - """ + """Get the list of tax accounts for a specific company.""" tax_accounts_dict = frappe._dict() tax_accounts_list = frappe.get_all("UAE VAT Account", filters={"parent": company}, @@ -67,11 +60,7 @@ def get_tax_accounts(company): return tax_accounts_dict def update_grand_total_for_rcm(doc, method): - """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form - - Args: - doc (Document): The document for the current Purchase Invoice - """ + """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form.""" country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'United Arab Emirates': @@ -102,14 +91,7 @@ def update_grand_total_for_rcm(doc, method): update_totals(vat_tax, base_vat_tax, doc) def update_totals(vat_tax, base_vat_tax, doc): - """Update the grand total values in the form - - Args: - vat_tax (float): Vat Tax to be subtracted - base_vat_tax (float): Base Vat Tax to be subtracted - doc (Document): The document for the current Purchase Invoice - """ - + """Update the grand total values in the form.""" doc.base_grand_total -= base_vat_tax doc.grand_total -= vat_tax @@ -130,16 +112,7 @@ def update_totals(vat_tax, base_vat_tax, doc): doc.set_payment_schedule() def make_regional_gl_entries(gl_entries, doc): - """This method is hooked to the make_regional_gl_entries in Purchase Invoice. - It appends the region specific general ledger entries to the list of GL Entries. - - Args: - gl_entries (List): List of GL entries to be made - doc (Document): The document for the current Purchase Invoice - - Returns: - List: Updates list of GL Entries - """ + """Hooked to make_regional_gl_entries in Purchase Invoice.It appends the region specific general ledger entries to the list of GL Entries.""" country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'United Arab Emirates': @@ -170,7 +143,7 @@ def make_regional_gl_entries(gl_entries, doc): return gl_entries def validate_returns(doc, method): - print("validate_returns") + """Sum of Tourist Returns and Standard Rated Expenses should be less than Total Tax.""" country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'United Arab Emirates': From 10a3a338b6a98d68e3d68d090801d08e7d240891 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Wed, 30 Sep 2020 21:51:00 +0530 Subject: [PATCH 022/229] refactor: change uae vat settingplural --- .../{uae_vat_setting => uae_vat_settings}/__init__.py | 0 .../test_uae_vat_settings.py} | 2 +- .../uae_vat_settings.js} | 2 +- .../uae_vat_settings.json} | 10 +++++----- .../uae_vat_settings.py} | 2 +- erpnext/regional/united_arab_emirates/setup.py | 2 +- erpnext/regional/united_arab_emirates/utils.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename erpnext/regional/doctype/{uae_vat_setting => uae_vat_settings}/__init__.py (100%) rename erpnext/regional/doctype/{uae_vat_setting/test_uae_vat_setting.py => uae_vat_settings/test_uae_vat_settings.py} (80%) rename erpnext/regional/doctype/{uae_vat_setting/uae_vat_setting.js => uae_vat_settings/uae_vat_settings.js} (80%) rename erpnext/regional/doctype/{uae_vat_setting/uae_vat_setting.json => uae_vat_settings/uae_vat_settings.json} (84%) rename erpnext/regional/doctype/{uae_vat_setting/uae_vat_setting.py => uae_vat_settings/uae_vat_settings.py} (88%) diff --git a/erpnext/regional/doctype/uae_vat_setting/__init__.py b/erpnext/regional/doctype/uae_vat_settings/__init__.py similarity index 100% rename from erpnext/regional/doctype/uae_vat_setting/__init__.py rename to erpnext/regional/doctype/uae_vat_settings/__init__.py diff --git a/erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py b/erpnext/regional/doctype/uae_vat_settings/test_uae_vat_settings.py similarity index 80% rename from erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py rename to erpnext/regional/doctype/uae_vat_settings/test_uae_vat_settings.py index a5f151dc73..b88439f9b8 100644 --- a/erpnext/regional/doctype/uae_vat_setting/test_uae_vat_setting.py +++ b/erpnext/regional/doctype/uae_vat_settings/test_uae_vat_settings.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestUAEVATSetting(unittest.TestCase): +class TestUAEVATSettings(unittest.TestCase): pass diff --git a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js similarity index 80% rename from erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js rename to erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js index f910e1f662..07a93010b5 100644 --- a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.js +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js @@ -1,7 +1,7 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('UAE VAT Setting', { +frappe.ui.form.on('UAE VAT Settings', { // refresh: function(frm) { // } diff --git a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json similarity index 84% rename from erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json rename to erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json index e2e2ab8245..ce2c1d4e14 100644 --- a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.json +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json @@ -7,7 +7,7 @@ "engine": "InnoDB", "field_order": [ "company", - "uae_vat_account" + "uae_vat_accounts" ], "fields": [ { @@ -20,19 +20,19 @@ "unique": 1 }, { - "fieldname": "uae_vat_account", + "fieldname": "uae_vat_accounts", "fieldtype": "Table", - "label": "UAE VAT Account", + "label": "UAE VAT Accounts", "options": "UAE VAT Account", "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-28 12:19:11.493138", + "modified": "2020-09-30 20:08:18.764798", "modified_by": "Administrator", "module": "Regional", - "name": "UAE VAT Setting", + "name": "UAE VAT Settings", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.py similarity index 88% rename from erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py rename to erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.py index 9549de9466..20dc604510 100644 --- a/erpnext/regional/doctype/uae_vat_setting/uae_vat_setting.py +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class UAEVATSetting(Document): +class UAEVATSettings(Document): pass diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 70030c736c..0de42120c4 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -145,7 +145,7 @@ def add_custom_roles_for_reports(): def add_permissions(): """Add Permissions for UAE VAT Settings and UAE VAT Account.""" - for doctype in ('UAE VAT Setting', 'UAE VAT Account'): + for doctype in ('UAE VAT Settings', 'UAE VAT Account'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index f75efd1aae..5a8c3c47fb 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -52,7 +52,7 @@ def get_tax_accounts(company): ) if not tax_accounts_list and not frappe.flags.in_test: - frappe.throw(_(f'Please set Vat Accounts for Company: "{company}" in UAE VAT Setting')) + frappe.throw(_(f'Please set Vat Accounts for Company: "{company}" in UAE VAT Settings')) for d in tax_accounts_list: for key, name in d.items(): tax_accounts_dict[name] = name From 8cc6f2b38881f1bcd53cf558a362486e564648f1 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Thu, 1 Oct 2020 12:44:39 +0530 Subject: [PATCH 023/229] feat(UAE VAT 21): add fixed width to columns --- .../report/uae_vat_21/uae_vat_21.html | 31 ++++++---- .../regional/report/uae_vat_21/uae_vat_21.js | 1 - .../regional/report/uae_vat_21/uae_vat_21.py | 57 +++++++++++-------- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.html b/erpnext/regional/report/uae_vat_21/uae_vat_21.html index aa6a255932..d9b9968d90 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.html +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.html @@ -1,21 +1,27 @@ {% var report_columns = report.get_columns_for_print(); report_columns = report_columns.filter(col => !col.hidden); - - if (report_columns.length > 8) { - frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application.")); - } %} +

{%= __(report.report_name) %}

-

{%= __("VAT on Sales and All Other Outputs") %}

+

{%= __("VAT on Sales and All Other Outputs") %}

- {% for (let i=0; i{%= report_columns[i].label %} + + + + {% for (let i=2; i{%= report_columns[i].label %} {% } %} @@ -38,17 +44,20 @@
{%= report_columns[0].label %}{%= report_columns[1].label %}
-

{%= __("VAT on Expenses and All Other Inputs") %}

+

{%= __("VAT on Expenses and All Other Inputs") %}

- {% for (let i=0; i{%= report_columns[i].label %} + + + + {% for (let i=2; i{%= report_columns[i].label %} {% } %} - {% for (let j=13; j Date: Thu, 1 Oct 2020 12:59:39 +0530 Subject: [PATCH 024/229] feat(UAE VAT 21): Add currency formatting --- .../regional/report/uae_vat_21/uae_vat_21.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.py b/erpnext/regional/report/uae_vat_21/uae_vat_21.py index 211bf16734..2bded71ec2 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.py +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.py @@ -60,8 +60,10 @@ def get_data(filters = None): emirate, amount, vat= d amounts_by_emirate[emirate] = { "legend": emirate, - "amount": amount, - "vat_amount": vat + "raw_amount": amount, + "raw_vat_amount": vat, + "amount": frappe.format(amount, 'Currency'), + "vat_amount": frappe.format(vat, 'Currency'), } for d, emirate in enumerate(emirates, 97): @@ -74,8 +76,8 @@ def get_data(filters = None): { "no": _(f'1{chr(d)}'), "legend": _(f'Standard rated supplies in {emirate}'), - "amount": 0, - "vat_amount": 0 + "amount": frappe.format(0, 'Currency'), + "vat_amount": frappe.format(0, 'Currency') } ) @@ -83,8 +85,8 @@ def get_data(filters = None): { "no": _('2'), "legend": _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), - "amount": (-1) * get_tourist_tax_return_total(filters), - "vat_amount": (-1) * get_tourist_tax_return_tax(filters) + "amount": frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), + "vat_amount": frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency') } ) @@ -92,8 +94,8 @@ def get_data(filters = None): { "no": _('3'), "legend": _('Supplies subject to the reverse charge provision'), - "amount": get_reverse_charge_total(filters), - "vat_amount": get_reverse_charge_tax(filters) + "amount": frappe.format(get_reverse_charge_total(filters), 'Currency'), + "vat_amount": frappe.format(get_reverse_charge_tax(filters), 'Currency') } ) @@ -101,7 +103,7 @@ def get_data(filters = None): { "no": _('4'), "legend": _('Zero Rated'), - "amount": get_zero_rated_total(filters), + "amount": frappe.format(get_zero_rated_total(filters), 'Currency'), "vat_amount": "-" } ) @@ -110,7 +112,7 @@ def get_data(filters = None): { "no": _('5'), "legend": _('Exempt Supplies'), - "amount": get_exempt_total(filters), + "amount": frappe.format(get_exempt_total(filters), 'Currency'), "vat_amount": "-" } ) @@ -133,8 +135,8 @@ def get_data(filters = None): { "no": '9', "legend": _('Standard Rated Expenses'), - "amount": get_standard_rated_expenses_total(filters), - "vat_amount": get_standard_rated_expenses_tax(filters) + "amount": frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), + "vat_amount": frappe.format(get_standard_rated_expenses_tax(filters), 'Currency') } ) @@ -142,8 +144,8 @@ def get_data(filters = None): { "no": _('10'), "legend": _('Supplies subject to the reverse charge provision'), - "amount": get_reverse_charge_recoverable_total(filters), - "vat_amount": get_reverse_charge_recoverable_tax(filters) + "amount": frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), + "vat_amount": frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency') } ) @@ -157,8 +159,8 @@ def get_chart(emirates, amounts_by_emirate): vat_amount = [] for d in emirates: if d in amounts_by_emirate: - amount.append(amounts_by_emirate[d]["amount"]) - vat_amount.append(amounts_by_emirate[d]["vat_amount"]) + amount.append(amounts_by_emirate[d]["raw_amount"]) + vat_amount.append(amounts_by_emirate[d]["raw_vat_amount"]) labels.append(d) datasets = [] From 51c6cf692ef4cc550fbc034142b25c819e7cc341 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Oct 2020 23:20:36 +0530 Subject: [PATCH 025/229] fix: Add method for loan closure --- erpnext/loan_management/doctype/loan/loan.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index d1b7589a17..1fb0805d38 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class Loan(AccountsController): def validate(self): @@ -182,6 +183,19 @@ def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) return monthly_repayment_amount +@frappe.whitelist() +def request_loan_closure(loan): + amounts = calculate_amounts(loan, getdate()) + + pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest'] + + # checking greater than 0 as there may be some minor precision error + if pending_amount > 0: + frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) + else: + # update status as loan closure requested + frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + @frappe.whitelist() def get_loan_application(loan_application): loan = frappe.get_doc("Loan Application", loan_application) From 6d27adccfce718c37b0a019fdf164c685c8271eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Oct 2020 23:28:39 +0530 Subject: [PATCH 026/229] fix: Button to close loan --- erpnext/loan_management/doctype/loan/loan.js | 40 ++++++++++++++++---- erpnext/loan_management/loan_common.js | 2 +- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 9b4c21770e..682b574fea 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -21,6 +21,14 @@ frappe.ui.form.on('Loan', { }; }); + frm.set_query("loan_type", function () { + return { + "filters": { + "docstatus": 1 + } + }; + }); + $.each(["penalty_income_account", "interest_income_account"], function(i, field) { frm.set_query(field, function () { return { @@ -49,19 +57,21 @@ frappe.ui.form.on('Loan', { refresh: function (frm) { if (frm.doc.docstatus == 1) { + if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) { + frm.add_custom_button(__('Request Loan Closure'), function() { + frm.trigger("request_loan_closure"); + },__('Status')); + frm.add_custom_button(__('Loan Repayment'), function() { + frm.trigger("make_repayment_entry"); + },__('Create')); + } + if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') { frm.add_custom_button(__('Loan Disbursement'), function() { frm.trigger("make_loan_disbursement"); },__('Create')); } - if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) { - frm.add_custom_button(__('Loan Repayment'), function() { - frm.trigger("make_repayment_entry"); - },__('Create')); - - } - if (frm.doc.status == "Loan Closure Requested") { frm.add_custom_button(__('Loan Security Unpledge'), function() { frm.trigger("create_loan_security_unpledge"); @@ -117,6 +127,22 @@ frappe.ui.form.on('Loan', { }) }, + request_loan_closure: function(frm) { + frappe.confirm(__("Do you really want to close this loan"), + function() { + frappe.call({ + args: { + 'loan': frm.doc.name + }, + method: "erpnext.loan_management.doctype.loan.loan.request_loan_closure", + callback: function() { + frm.reload_doc(); + } + }); + } + ); + }, + create_loan_security_unpledge: function(frm) { frappe.call({ method: "erpnext.loan_management.doctype.loan.loan.unpledge_security", diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js index d9dd415296..33a5de0566 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -15,7 +15,7 @@ frappe.ui.form.on(cur_frm.doctype, { frappe.route_options = { voucher_no: frm.doc.name, company: frm.doc.company, - from_date: frm.doc.posting_date, + from_date: moment(frm.doc.posting_date).format('YYYY-MM-DD'), to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), show_cancelled_entries: frm.doc.docstatus === 2 }; From 6dccf79250ff1a380b712a7ff3625a648d7f71f3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Oct 2020 23:32:57 +0530 Subject: [PATCH 027/229] fix: Add accrual type and penalty field in interest accrual --- .../loan_disbursement/loan_disbursement.py | 2 +- .../loan_disbursement/test_loan_disbursement.py | 3 +-- .../loan_interest_accrual.json | 17 ++++++++++++++++- .../process_loan_interest_accrual.json | 10 +++++++++- .../process_loan_interest_accrual.py | 5 +++-- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 260fada893..bfdc8b403e 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -74,7 +74,7 @@ class LoanDisbursement(AccountsController): if loan_details.status == "Disbursed" and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), - loan=self.against_loan) + loan=self.against_loan, accrual_type="Disbursement") if disbursed_amount > loan_details.loan_amount: topup_amount = disbursed_amount - loan_details.loan_amount diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 2cb2637612..3ade5c549b 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -60,8 +60,7 @@ class TestLoanDisbursement(unittest.TestCase): self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), - "Regular Payment", 611095.89) + repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89) repayment_entry.submit() loan.reload() diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index 5fc3e8f4b6..a0abc5b57e 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -14,6 +14,7 @@ "column_break_4", "company", "posting_date", + "accrual_type", "is_term_loan", "section_break_7", "pending_principal_amount", @@ -22,6 +23,7 @@ "column_break_14", "interest_amount", "paid_interest_amount", + "penalty_amount", "section_break_15", "process_loan_interest_accrual", "repayment_schedule_name", @@ -149,12 +151,25 @@ "fieldtype": "Currency", "label": "Paid Interest Amount", "options": "Company:company:default_currency" + }, + { + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nDisbursement" + }, + { + "fieldname": "penalty_amount", + "fieldtype": "Currency", + "label": "Penalty Amount", + "options": "Company:company:default_currency" } ], "in_create": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-16 11:24:23.258404", + "modified": "2020-10-10 03:12:58.204501", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 0ef098f278..1e05a42de7 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -10,6 +10,7 @@ "loan_type", "loan", "process_type", + "accrual_type", "amended_from" ], "fields": [ @@ -47,11 +48,18 @@ "hidden": 1, "label": "Process Type", "read_only": 1 + }, + { + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nAccrual" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-09 22:52:53.911416", + "modified": "2020-10-08 12:20:11.124769", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 0fa96860d0..1eeb18b7ea 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -20,19 +20,20 @@ class ProcessLoanInterestAccrual(Document): if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type) + open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, loan_type=self.loan_type) -def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None): +def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"): loan_process = frappe.new_doc('Process Loan Interest Accrual') loan_process.posting_date = posting_date or nowdate() loan_process.loan_type = loan_type loan_process.process_type = 'Demand Loans' loan_process.loan = loan + loan_process.accrual_type = accrual_type loan_process.submit() From d86b7c55373c0a6e72d18ab0f9106649489575d5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 11 Oct 2020 00:37:34 +0530 Subject: [PATCH 028/229] fix: Remove repayment type --- .../loan_management/doctype/loan/test_loan.py | 136 ++++++++++++++---- .../loan_interest_accrual.py | 29 +++- .../loan_repayment/loan_repayment.json | 31 ++-- .../doctype/loan_repayment/loan_repayment.py | 116 +++++++++------ 4 files changed, 220 insertions(+), 92 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5a4a19a0fb..7b653652ea 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import unpledge_security +from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount @@ -132,7 +132,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -149,23 +149,23 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111118.68) repayment_entry.save() repayment_entry.submit() penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount']) loan.load_from_db() - self.assertEquals(amounts[0], repayment_entry.interest_payable) + total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount'] + self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable) self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid - - penalty_amount - amounts[0], 2)) + penalty_amount - total_interest_paid, 2)) - def test_loan_closure_repayment(self): + def test_loan_closure(self): pledge = [{ "loan_security": "Test Security 1", "qty": 4000.00 @@ -174,7 +174,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -196,14 +196,16 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), - "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) - self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) + self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + request_loan_closure(loan.name) loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -230,8 +232,7 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), - "Regular Payment", 89768.75) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75) repayment_entry.submit() @@ -281,7 +282,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -299,10 +300,10 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), - "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() + request_loan_closure(loan.name) loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -317,9 +318,9 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) - amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") - self.assertEqual(amounts['pending_principal_amount'], 0) - self.assertEqual(amounts['payable_principal_amount'], 0) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertTrue(amounts['pending_principal_amount'] < 0) + self.assertTrue(amounts['payable_principal_amount'] < 0) self.assertEqual(amounts['interest_amount'], 0) def test_disbursal_check_with_shortfall(self): @@ -381,7 +382,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -399,20 +400,102 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), - "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', 'paid_principal_amount']) + request_loan_closure(loan.name) loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") - self.assertEquals(amounts['pending_principal_amount'], 0.0) + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertTrue(amounts['pending_principal_amount'] < 0.0) + + def test_partial_unaccrued_interest_payment(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 5.5 + + # get partial unaccrued interest amount + paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + paid_amount) + + repayment_entry.submit() + repayment_entry.load_from_db() + + partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2) + self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) + + def test_penalty(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 1)) + paid_amount = amounts['interest_amount']/2 + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + paid_amount) + + repayment_entry.submit() + + # 30 days - grace period + penalty_days = 30 - 5 + penalty_applicable_amount = flt(amounts['interest_amount']/2, 2) + penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days)/365, 2) + process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') + + calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', + {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') + + self.assertEquals(calculated_penalty_amount, penalty_amount) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): @@ -582,12 +665,11 @@ def create_loan_security_price(loan_security, loan_security_price, uom, from_dat "valid_upto": to_date }).insert(ignore_permissions=True) -def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount): +def create_repayment_entry(loan, applicant, posting_date, paid_amount): lr = frappe.get_doc({ "doctype": "Loan Repayment", "against_loan": loan, - "payment_type": payment_type, "company": "_Test Company", "posting_date": posting_date or nowdate(), "applicant": applicant, diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 2d959bf3be..4517de0c59 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -22,7 +22,6 @@ class LoanInterestAccrual(AccountsController): if not self.interest_amount and not self.payable_principal_amount: frappe.throw(_("Interest Amount or Principal Amount is mandatory")) - def on_submit(self): self.make_gl_entries() @@ -79,8 +78,11 @@ class LoanInterestAccrual(AccountsController): # For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and # rate of interest is 13.5 then first loan interest accural will be on '01-10-2019' # which means interest will be accrued for 30 days which should be equal to 11095.89 -def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest): +def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type): + from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts + no_of_days = get_no_of_days_for_interest_accural(loan, posting_date) + precision = cint(frappe.db.get_default("currency_precision")) or 2 if no_of_days <= 0: return @@ -91,7 +93,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i else: pending_principal_amount = loan.disbursed_amount - interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) + interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) payable_interest = interest_per_day * no_of_days args = frappe._dict({ @@ -102,13 +104,16 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i 'loan_account': loan.loan_account, 'pending_principal_amount': pending_principal_amount, 'interest_amount': payable_interest, + 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'], 'process_loan_interest': process_loan_interest, - 'posting_date': posting_date + 'posting_date': posting_date, + 'accrual_type': accrual_type }) - make_loan_interest_accrual_entry(args) + if flt(payable_interest, precision) > 0.0: + make_loan_interest_accrual_entry(args) -def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): +def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"): query_filters = { "status": ('in', ['Disbursed', 'Partially Disbursed']), "docstatus": 1 @@ -127,7 +132,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte filters=query_filters) for loan in open_loans: - calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest) + calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type) def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None): curr_date = posting_date or add_days(nowdate(), 1) @@ -192,10 +197,12 @@ def make_loan_interest_accrual_entry(args): loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) + loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision) loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name loan_interest_accrual.payable_principal_amount = args.payable_principal + loan_interest_accrual.accrual_type = args.accrual_type loan_interest_accrual.save() loan_interest_accrual.submit() @@ -226,3 +233,11 @@ def days_in_year(year): return days +def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): + if not posting_date: + posting_date = getdate() + + precision = cint(frappe.db.get_default("currency_precision")) or 2 + + return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), 2) + diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 5942455919..60b20369dc 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -10,11 +10,11 @@ "applicant_type", "applicant", "loan_type", - "payment_type", "column_break_3", "company", "posting_date", "is_term_loan", + "rate_of_interest", "payment_details_section", "due_date", "pending_principal_amount", @@ -31,6 +31,7 @@ "column_break_21", "reference_date", "principal_amount_paid", + "total_interest_paid", "repayment_details", "amended_from" ], @@ -95,15 +96,6 @@ "fieldname": "column_break_9", "fieldtype": "Column Break" }, - { - "default": "Regular Payment", - "fieldname": "payment_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Payment Type", - "options": "\nRegular Payment\nLoan Closure", - "reqd": 1 - }, { "fieldname": "payable_amount", "fieldtype": "Currency", @@ -195,6 +187,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Principal Amount Paid", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -217,11 +210,27 @@ "hidden": 1, "label": "Repayment Details", "options": "Loan Repayment Detail" + }, + { + "fieldname": "total_interest_paid", + "fieldtype": "Currency", + "hidden": 1, + "label": "Total Interest Paid", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fetch_from": "loan_type.rate_of_interest", + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "label": "Rate Of Interest", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-16 09:40:15.581165", + "modified": "2020-10-10 03:49:01.827593", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 97dbc44bf1..c8344d4667 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -14,19 +14,18 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest class LoanRepayment(AccountsController): def validate(self): - amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type) + amounts = calculate_amounts(self.against_loan, self.posting_date) self.set_missing_values(amounts) self.validate_amount() - self.allocate_amounts(amounts['pending_accrual_entries']) - - def before_submit(self): - self.book_unaccrued_interest() + self.allocate_amounts(amounts) def on_submit(self): + self.book_unaccrued_interest() self.update_paid_amount() self.make_gl_entries() @@ -72,29 +71,35 @@ class LoanRepayment(AccountsController): msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) frappe.throw(msg) - if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision): - msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) - frappe.throw(msg) - def book_unaccrued_interest(self): - if self.payment_type == 'Loan Closure': - total_interest_paid = 0 - for payment in self.repayment_details: - total_interest_paid += payment.paid_interest_amount + precision = cint(frappe.db.get_default("currency_precision")) or 2 + if self.total_interest_paid > self.interest_payable: + if not self.is_term_loan: + # get last loan interest accrual date + last_accrual_date = frappe.get_value('Loan Interest Accrual', {'loan': self.against_loan}, 'MAX(posting_date)') - if total_interest_paid < self.interest_payable: - if not self.is_term_loan: - process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, - loan=self.against_loan) + # get posting date upto which interest has to be accrued + per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date), 2) - lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': - process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, + precision)/per_day_interest, 0) - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': lia.interest_amount, - 'paid_principal_amount': lia.payable_principal_amount - }) + posting_date = add_days(last_accrual_date, no_of_days) + + # book excess interest paid + process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date, + loan=self.against_loan, accrual_type="Repayment") + + # get loan interest accrual to update paid amount + lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': + process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), + 'paid_principal_amount': 0.0 + }) def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -108,12 +113,6 @@ class LoanRepayment(AccountsController): WHERE name = %s""", (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual)) - if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision): - if loan.is_secured_loan: - frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") - else: - frappe.db.set_value("Loan", self.against_loan, "status", "Closed") - frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, loan.total_principal_paid + self.principal_amount_paid, self.against_loan)) @@ -137,15 +136,17 @@ class LoanRepayment(AccountsController): if loan.status == "Loan Closure Requested": frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") - def allocate_amounts(self, paid_entries): + def allocate_amounts(self, repayment_details): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + self.set('repayment_details', []) self.principal_amount_paid = 0 total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount - if self.amount_paid - self.penalty_amount > 0 and paid_entries: + if self.amount_paid - self.penalty_amount > 0: interest_paid = self.amount_paid - self.penalty_amount - for lia, amounts in iteritems(paid_entries): + for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid: interest_amount = amounts['interest_amount'] paid_principal = amounts['payable_principal_amount'] @@ -169,9 +170,24 @@ class LoanRepayment(AccountsController): 'paid_principal_amount': paid_principal }) - if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: - unaccrued_interest = self.interest_payable - total_interest_paid - interest_paid -= unaccrued_interest + if repayment_details['unaccrued_interest'] and interest_paid: + # no of days for which to accrue interest + # Interest can only be accrued for an entire day and not partial + if interest_paid > repayment_details['unaccrued_interest']: + per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date), precision) + interest_paid -= repayment_details['unaccrued_interest'] + total_interest_paid += repayment_details['unaccrued_interest'] + else: + # get no of days for which interest can be paid + per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date), precision) + + no_of_days = cint(interest_paid/per_day_interest) + total_interest_paid += no_of_days * per_day_interest + interest_paid -= no_of_days * per_day_interest + + self.total_interest_paid = total_interest_paid if interest_paid: self.principal_amount_paid += interest_paid @@ -289,7 +305,7 @@ def get_accrued_interest_entries(against_loan): # This function returns the amounts that are payable at the time of loan repayment based on posting date # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable -def get_amounts(amounts, against_loan, posting_date, payment_type): +def get_amounts(amounts, against_loan, posting_date): precision = cint(frappe.db.get_default("currency_precision")) or 2 against_loan_doc = frappe.get_doc("Loan", against_loan) @@ -332,15 +348,16 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): else: pending_principal_amount = against_loan_doc.disbursed_amount - if payment_type == "Loan Closure": - if due_date: - pending_days = date_diff(posting_date, due_date) + 1 - else: - pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 + unaccrued_interest = 0 + if due_date: + pending_days = date_diff(posting_date, due_date) + 1 + else: + pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 - payable_principal_amount = pending_principal_amount - per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 - total_pending_interest += (pending_days * per_day_interest) + if pending_days > 0: + payable_principal_amount = flt(pending_principal_amount, precision) + per_day_interest = get_per_day_interest(payable_principal_amount, loan_type_details.rate_of_interest, posting_date) + unaccrued_interest += (pending_days * flt(per_day_interest, precision)) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) @@ -348,6 +365,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): amounts["penalty_amount"] = flt(penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["pending_accrual_entries"] = pending_accrual_entries + amounts["unaccrued_interest"] = unaccrued_interest if final_due_date: amounts["due_date"] = final_due_date @@ -355,7 +373,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): return amounts @frappe.whitelist() -def calculate_amounts(against_loan, posting_date, payment_type): +def calculate_amounts(against_loan, posting_date, payment_type=''): amounts = { 'penalty_amount': 0.0, @@ -363,10 +381,14 @@ def calculate_amounts(against_loan, posting_date, payment_type): 'pending_principal_amount': 0.0, 'payable_principal_amount': 0.0, 'payable_amount': 0.0, + 'unaccrued_interest': 0.0, 'due_date': '' } - amounts = get_amounts(amounts, against_loan, posting_date, payment_type) + amounts = get_amounts(amounts, against_loan, posting_date) + + if payment_type == 'Loan Closure': + amounts['payable_amount'] += amounts['unaccrued_interest'] return amounts From ac451b3a5435b2927f1a60f3e0901a45c3fceb07 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Sun, 11 Oct 2020 01:40:22 +0530 Subject: [PATCH 029/229] feat(UAE VAT 21): Move standard rated expense from Sales invoice to Purchase invoice --- erpnext/hooks.py | 4 +--- erpnext/regional/report/uae_vat_21/uae_vat_21.py | 4 ++-- erpnext/regional/united_arab_emirates/setup.py | 7 ++++--- erpnext/regional/united_arab_emirates/utils.py | 10 ++++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a0242c175c..8d08c90047 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -242,15 +242,13 @@ doc_events = { "erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.erpnext_integrations.taxjar_integration.delete_transaction" ], - "validate": [ - "erpnext.regional.united_arab_emirates.utils.validate_returns", - ], "on_trash": "erpnext.regional.check_deletion_permission" }, "Purchase Invoice": { "validate": [ "erpnext.regional.india.utils.update_grand_total_for_rcm", "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", + "erpnext.regional.united_arab_emirates.utils.validate_returns" ] }, "Payment Entry": { diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.py b/erpnext/regional/report/uae_vat_21/uae_vat_21.py index 2bded71ec2..227c2fb5a1 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.py +++ b/erpnext/regional/report/uae_vat_21/uae_vat_21.py @@ -277,7 +277,7 @@ def get_standard_rated_expenses_total(filters): conditions = get_conditions(filters) return frappe.db.sql(""" select sum(total) from - `tabSales Invoice` + `tabPurchase Invoice` where standard_rated_expenses > 0 and docstatus = 1 {where_conditions} ; @@ -288,7 +288,7 @@ def get_standard_rated_expenses_tax(filters): conditions = get_conditions(filters) return frappe.db.sql(""" select sum(standard_rated_expenses) from - `tabSales Invoice` + `tabPurchase Invoice` where standard_rated_expenses > 0 and docstatus = 1 {where_conditions} ; diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 0de42120c4..e48b9a9ce1 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -38,8 +38,11 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Read Only', insert_after='supplier_name', fetch_from='supplier.supplier_name_in_arabic', print_hide=1), + dict(fieldname='standard_rated_expenses', label='Standard Rated Expenses (AED)', + insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0', + depends_on="eval:doc.reverse_charge=='N'",), dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='permit_no', print_hide=1, + fieldtype='Select', insert_after='standard_rated_expenses', print_hide=1, options='Y\nN', default='N'), dict(fieldname='claimable_reverse_charge', label='Claimable Reverse Charge (Percentage)', insert_after='reverse_charge', fieldtype='Percent', print_hide=1, @@ -57,8 +60,6 @@ def make_custom_fields(): fieldtype='Read Only', fetch_from='customer_address.emirates'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), - dict(fieldname='standard_rated_expenses', label='Standard Rated Expenses (AED)', - insert_after='tourist_tax_return', fieldtype='Currency', print_hide=1, default='0'), ] invoice_item_fields = [ diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 5a8c3c47fb..dbe283f226 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -143,11 +143,13 @@ def make_regional_gl_entries(gl_entries, doc): return gl_entries def validate_returns(doc, method): - """Sum of Tourist Returns and Standard Rated Expenses should be less than Total Tax.""" + """Standard Rated expenses should not be set when Reverse Charge Applicable is set.""" country = frappe.get_cached_value('Company', doc.company, 'country') - + print("-"*50) + print(doc.reverse_charge) + print(flt(doc.standard_rated_expenses)) if country != 'United Arab Emirates': return - if flt(doc.tourist_tax_return) + flt(doc.standard_rated_expenses) > flt(doc.total_taxes_and_charges): - frappe.throw(_("The Total Returns(Tax Refund provided to Tourists (AED) + Standard Rated Expenses (AED)) should be less than the Total Taxes and Charges (Company Currency)")) \ No newline at end of file + if doc.reverse_charge == 'Y' and flt(doc.standard_rated_expenses) != 0: + frappe.throw(_("Standard Rated expenses should not be set when Reverse Charge Applicable is Y")) \ No newline at end of file From a034311b1b397b26217c47b16e20ae8581ac1b0d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 11 Oct 2020 20:52:02 +0530 Subject: [PATCH 030/229] fix: Acrual type --- .../doctype/loan_interest_accrual/loan_interest_accrual.json | 2 +- .../process_loan_interest_accrual.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index a0abc5b57e..893609e0c7 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -169,7 +169,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-10 03:12:58.204501", + "modified": "2020-10-11 11:17:44.704694", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 1e05a42de7..bb781b1d56 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -53,13 +53,13 @@ "fieldname": "accrual_type", "fieldtype": "Select", "label": "Accrual Type", - "options": "Regular\nRepayment\nAccrual" + "options": "Regular\nRepayment\nDisbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-08 12:20:11.124769", + "modified": "2020-10-11 11:19:00.531046", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", From a322f0d0b3163192b372a1b407468f12c9474334 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Mon, 12 Oct 2020 11:38:55 +0800 Subject: [PATCH 031/229] chore: Fix error message grammar --- erpnext/stock/doctype/item_price/item_price.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 8e39eb5037..56efa09486 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -23,7 +23,7 @@ class ItemPrice(Document): def validate_item(self): if not frappe.db.exists("Item", self.item_code): - frappe.throw(_("Item {0} not found").format(self.item_code)) + frappe.throw(_("Item {0} not found.").format(self.item_code)) def validate_dates(self): if self.valid_from and self.valid_upto: @@ -38,8 +38,7 @@ class ItemPrice(Document): if not price_list_details: link = frappe.utils.get_link_to_form('Price List', self.price_list) - frappe.throw("The price list {0} does not exists or disabled". - format(link)) + frappe.throw("The price list {0} does not exist or is disabled".format(link)) self.buying, self.selling, self.currency = price_list_details @@ -69,7 +68,7 @@ class ItemPrice(Document): self.reference = self.customer if self.buying: self.reference = self.supplier - + if self.selling and not self.buying: # if only selling then remove supplier self.supplier = None From 39bed3a65e2557d15d2275178120de941e66c536 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Mon, 12 Oct 2020 11:42:05 +0800 Subject: [PATCH 032/229] fix: Add null or empty checking to validation This commit adds null or empty checking in the **Item Price** DocType's `check_duplicates()`. This was done to fix a bug where some prices are shown to be duplciates even though they aren't because they don't have values in some fields. --- .../stock/doctype/item_price/item_price.py | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 56efa09486..f88b05cd20 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -48,20 +48,52 @@ class ItemPrice(Document): self.item_code,["item_name", "description"]) def check_duplicates(self): - conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s" + conditions = """ + where + item_code = %(item_code)s + and price_list = %(price_list)s + and name != %(name)s + """ - for field in ['uom', 'valid_from', - 'valid_upto', 'packing_unit', 'customer', 'supplier']: + for field in [ + "uom", + "valid_from", + "valid_upto", + "packing_unit", + "customer", + "supplier", + ]: if self.get(field): - conditions += " and {0} = %({1})s".format(field, field) + conditions += " and {0} = %({0})s ".format(field) + else: + conditions += """ + and ( + isnull({0}) + or {0} = '' + ) + """.format( + field + ) - price_list_rate = frappe.db.sql(""" - SELECT price_list_rate - FROM `tabItem Price` - {conditions} """.format(conditions=conditions), self.as_dict()) + price_list_rate = frappe.db.sql( + """ + select + price_list_rate + from + `tabItem Price` + {conditions} + """.format( + conditions=conditions + ), + self.as_dict(), + ) - if price_list_rate : - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem) + if price_list_rate: + frappe.throw(_(""" + Item Price appears multiple times based on + Price List, Supplier/Customer, Currency, Item, UOM, Qty, + and Dates. + """), ItemPriceDuplicateItem,) def before_save(self): if self.selling: From 58389ebd402d461903fccb522127bdc1adbbb40e Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Mon, 12 Oct 2020 15:20:11 +0800 Subject: [PATCH 033/229] style: Collapse whitespace --- .../stock/doctype/item_price/item_price.py | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index f88b05cd20..f05de22d01 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -66,21 +66,12 @@ class ItemPrice(Document): if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: - conditions += """ - and ( - isnull({0}) - or {0} = '' - ) - """.format( - field - ) + conditions += "and (isnull({0}) or {0} = '')".format(field) price_list_rate = frappe.db.sql( """ - select - price_list_rate - from - `tabItem Price` + select price_list_rate + from `tabItem Price` {conditions} """.format( conditions=conditions @@ -89,11 +80,7 @@ class ItemPrice(Document): ) if price_list_rate: - frappe.throw(_(""" - Item Price appears multiple times based on - Price List, Supplier/Customer, Currency, Item, UOM, Qty, - and Dates. - """), ItemPriceDuplicateItem,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: From f6bcd5c2b230375f4014e8270a41027da9b9c6b7 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Mon, 12 Oct 2020 16:57:33 +0800 Subject: [PATCH 034/229] test: Add test case for duplicate checking --- .../doctype/item_price/test_item_price.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 702acc38fe..f3d406eeca 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -138,4 +138,23 @@ class TestItemPrice(unittest.TestCase): # Valid price list must already exist self.assertRaises(frappe.ValidationError, doc.save) + def test_empty_duplicate_validation(self): + # Check if none/empty values are not compared during insert validation + doc = frappe.copy_doc(test_records[2]) + doc.customer = None + doc.price_list_rate = 21 + doc.insert() + + args = { + "price_list": doc.price_list, + "uom": "_Test UOM", + "transaction_date": '2017-04-18', + "qty": 7 + } + + price = get_price_list_rate_for(args, doc.item_code) + frappe.db.rollback() + + self.assertEqual(price, 21) + test_records = frappe.get_test_records('Item Price') From 8c13fded17c6a52c09ac5a37bf636b91b2076e8d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Oct 2020 09:23:04 +0530 Subject: [PATCH 035/229] fix: Add unaccrued interest in interest amount for loan closure --- erpnext/loan_management/doctype/loan_repayment/loan_repayment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c8344d4667..940f82ee34 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -389,6 +389,7 @@ def calculate_amounts(against_loan, posting_date, payment_type=''): if payment_type == 'Loan Closure': amounts['payable_amount'] += amounts['unaccrued_interest'] + amounts['interest_amount'] += amounts['unaccrued_interest'] return amounts From 66967c6d450f5d83cf6d999ecb4b5516fd71a050 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Oct 2020 18:10:51 +0530 Subject: [PATCH 036/229] fix: Loan Security unpledge on loan cancel --- erpnext/loan_management/doctype/loan/loan.js | 3 +++ erpnext/loan_management/doctype/loan/loan.py | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 682b574fea..0dc3bf8563 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -11,6 +11,9 @@ frappe.ui.form.on('Loan', { } }, onload: function (frm) { + // Ignore loan security pledge on cancel of loan + frm.ignore_doctypes_on_cancel_all = ["Loan Security Pledge"]; + frm.set_query("loan_application", function () { return { "filters": { diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 1fb0805d38..2d705fc296 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -138,9 +138,12 @@ class Loan(AccountsController): }) def unlink_loan_security_pledge(self): - frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET - loan = '', status = 'Unpledged' - where name = %s """, (self.loan_security_pledge)) + pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name}) + pledge_list = [d.name for d in pledges] + if pledge_list: + frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + loan = '', status = 'Unpledged' + where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) def update_total_amount_paid(doc): total_amount_paid = 0 From dfc3993cecdd7f0e7d9cbd80734fbf8c6b8021f2 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 14 Oct 2020 12:21:36 +0530 Subject: [PATCH 037/229] chore(UAE VAT 21): rename report --- erpnext/accounts/desk_page/accounting/accounting.json | 2 +- .../report/{uae_vat_21 => uae_vat_201}/__init__.py | 0 .../uae_vat_21.html => uae_vat_201/uae_vat_201.html} | 0 .../uae_vat_21.js => uae_vat_201/uae_vat_201.js} | 2 +- .../uae_vat_21.json => uae_vat_201/uae_vat_201.json} | 4 ++-- .../uae_vat_21.py => uae_vat_201/uae_vat_201.py} | 6 +++--- erpnext/regional/united_arab_emirates/setup.py | 10 +++++----- erpnext/regional/united_arab_emirates/utils.py | 8 ++------ 8 files changed, 14 insertions(+), 18 deletions(-) rename erpnext/regional/report/{uae_vat_21 => uae_vat_201}/__init__.py (100%) rename erpnext/regional/report/{uae_vat_21/uae_vat_21.html => uae_vat_201/uae_vat_201.html} (100%) rename erpnext/regional/report/{uae_vat_21/uae_vat_21.js => uae_vat_201/uae_vat_201.js} (96%) rename erpnext/regional/report/{uae_vat_21/uae_vat_21.json => uae_vat_201/uae_vat_201.json} (88%) rename erpnext/regional/report/{uae_vat_21/uae_vat_21.py => uae_vat_201/uae_vat_201.py} (98%) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 214e31f21f..7d25b27e77 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -83,7 +83,7 @@ { "hidden": 0, "label": "Value-Added Tax (VAT UAE)", - "links": "[\n {\n \"label\": \"UAE VAT Setting\",\n \"name\": \"UAE VAT Setting\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"UAE VAT 21\",\n \"name\": \"UAE VAT 21\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"label\": \"UAE VAT Setting\",\n \"name\": \"UAE VAT Setting\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", diff --git a/erpnext/regional/report/uae_vat_21/__init__.py b/erpnext/regional/report/uae_vat_201/__init__.py similarity index 100% rename from erpnext/regional/report/uae_vat_21/__init__.py rename to erpnext/regional/report/uae_vat_201/__init__.py diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.html b/erpnext/regional/report/uae_vat_201/uae_vat_201.html similarity index 100% rename from erpnext/regional/report/uae_vat_21/uae_vat_21.html rename to erpnext/regional/report/uae_vat_201/uae_vat_201.html diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.js b/erpnext/regional/report/uae_vat_201/uae_vat_201.js similarity index 96% rename from erpnext/regional/report/uae_vat_21/uae_vat_21.js rename to erpnext/regional/report/uae_vat_201/uae_vat_201.js index 5fc5449655..5957424770 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.js +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.js @@ -2,7 +2,7 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.query_reports["UAE VAT 21"] = { +frappe.query_reports["UAE VAT 201"] = { "filters": [ { "fieldname": "company", diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.json b/erpnext/regional/report/uae_vat_201/uae_vat_201.json similarity index 88% rename from erpnext/regional/report/uae_vat_21/uae_vat_21.json rename to erpnext/regional/report/uae_vat_201/uae_vat_201.json index 421990cc4a..8a88bcd3e2 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.json +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.json @@ -12,11 +12,11 @@ "modified": "2020-09-10 08:51:02.298482", "modified_by": "Administrator", "module": "Regional", - "name": "UAE VAT 21", + "name": "UAE VAT 201", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "GL Entry", - "report_name": "UAE VAT 21", + "report_name": "UAE VAT 201", "report_type": "Script Report", "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/uae_vat_21/uae_vat_21.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py similarity index 98% rename from erpnext/regional/report/uae_vat_21/uae_vat_21.py rename to erpnext/regional/report/uae_vat_201/uae_vat_201.py index 227c2fb5a1..bcd31351ea 100644 --- a/erpnext/regional/report/uae_vat_21/uae_vat_21.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -279,7 +279,7 @@ def get_standard_rated_expenses_total(filters): select sum(total) from `tabPurchase Invoice` where - standard_rated_expenses > 0 + claimable_standard_rated_expenses > 0 and docstatus = 1 {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 @@ -287,10 +287,10 @@ def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions(filters) return frappe.db.sql(""" - select sum(standard_rated_expenses) from + select sum(claimable_standard_rated_expenses) from `tabPurchase Invoice` where - standard_rated_expenses > 0 + claimable_standard_rated_expenses > 0 and docstatus = 1 {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index e48b9a9ce1..a0e7620919 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -38,11 +38,11 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Read Only', insert_after='supplier_name', fetch_from='supplier.supplier_name_in_arabic', print_hide=1), - dict(fieldname='standard_rated_expenses', label='Standard Rated Expenses (AED)', + dict(fieldname='claimable_standard_rated_expenses', label='Claimable Standard Rated Expenses (AED)', insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0', depends_on="eval:doc.reverse_charge=='N'",), dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='standard_rated_expenses', print_hide=1, + fieldtype='Select', insert_after='claimable_standard_rated_expenses', print_hide=1, options='Y\nN', default='N'), dict(fieldname='claimable_reverse_charge', label='Claimable Reverse Charge (Percentage)', insert_after='reverse_charge', fieldtype='Percent', print_hide=1, @@ -132,11 +132,11 @@ def add_print_formats(): name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) def add_custom_roles_for_reports(): - """Add Access Control to UAE VAT 21.""" - if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 21')): + """Add Access Control to UAE VAT 201.""" + if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 201')): frappe.get_doc(dict( doctype='Custom Role', - report='UAE VAT 21', + report='UAE VAT 201', roles= [ dict(role='Accounts User'), dict(role='Accounts Manager'), diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index dbe283f226..37501a2638 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -145,11 +145,7 @@ def make_regional_gl_entries(gl_entries, doc): def validate_returns(doc, method): """Standard Rated expenses should not be set when Reverse Charge Applicable is set.""" country = frappe.get_cached_value('Company', doc.company, 'country') - print("-"*50) - print(doc.reverse_charge) - print(flt(doc.standard_rated_expenses)) if country != 'United Arab Emirates': return - - if doc.reverse_charge == 'Y' and flt(doc.standard_rated_expenses) != 0: - frappe.throw(_("Standard Rated expenses should not be set when Reverse Charge Applicable is Y")) \ No newline at end of file + if doc.reverse_charge == 'Y' and flt(doc.claimable_standard_rated_expenses) != 0: + frappe.throw(_("Claimable Standard Rated expenses should not be set when Reverse Charge Applicable is Y")) \ No newline at end of file From b5d16a82eb73ab5a287a982d105bfeef65b3f3c6 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Oct 2020 14:05:54 +0530 Subject: [PATCH 038/229] fix: Dont overrule Item Price via Pricing Rule Rate if 0 --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 454776e4e7..540a9b2c91 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -342,8 +342,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args): pricing_rule_rate = 0.0 if pricing_rule.currency == args.currency: pricing_rule_rate = pricing_rule.rate + + if pricing_rule_rate: + # Override already set price list rate (from item price) + # if pricing_rule_rate > 0 + item_details.update({ + "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), + }) item_details.update({ - "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), "discount_percentage": 0.0 }) From b9dc13294d7233eebe37783394550a3bdbe0a334 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 14 Oct 2020 16:56:26 +0530 Subject: [PATCH 039/229] chore(UAE VAT 201) solve translations issues --- .../report/uae_vat_201/uae_vat_201.py | 24 +++++++++---------- .../regional/united_arab_emirates/utils.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index bcd31351ea..a2518c38c6 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -68,14 +68,14 @@ def get_data(filters = None): for d, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _(f'1{chr(d)}') - amounts_by_emirate[emirate]["legend"] = _(f'Standard rated supplies in {emirate}') + amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(d)) + amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) data.append(amounts_by_emirate[emirate]) else: data.append( { - "no": _(f'1{chr(d)}'), - "legend": _(f'Standard rated supplies in {emirate}'), + "no": _('1{0}').format(chr(d)), + "legend": _('Standard rated supplies in {0}').format(emirate), "amount": frappe.format(0, 'Currency'), "vat_amount": frappe.format(0, 'Currency') } @@ -83,7 +83,7 @@ def get_data(filters = None): data.append( { - "no": _('2'), + "no": '2', "legend": _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), "amount": frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), "vat_amount": frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency') @@ -92,7 +92,7 @@ def get_data(filters = None): data.append( { - "no": _('3'), + "no": '3', "legend": _('Supplies subject to the reverse charge provision'), "amount": frappe.format(get_reverse_charge_total(filters), 'Currency'), "vat_amount": frappe.format(get_reverse_charge_tax(filters), 'Currency') @@ -101,7 +101,7 @@ def get_data(filters = None): data.append( { - "no": _('4'), + "no": '4', "legend": _('Zero Rated'), "amount": frappe.format(get_zero_rated_total(filters), 'Currency'), "vat_amount": "-" @@ -110,7 +110,7 @@ def get_data(filters = None): data.append( { - "no": _('5'), + "no": '5', "legend": _('Exempt Supplies'), "amount": frappe.format(get_exempt_total(filters), 'Currency'), "vat_amount": "-" @@ -118,14 +118,14 @@ def get_data(filters = None): ) data.append({ - "no": _(''), - "legend": _(''), + "no": '', + "legend": '', "amount": '', "vat_amount": '' }) data.append({ - "no": _(''), + "no": '', "legend": _('VAT on Expenses and All Other Inputs'), "amount": '', "vat_amount": '' @@ -142,7 +142,7 @@ def get_data(filters = None): data.append( { - "no": _('10'), + "no": '10', "legend": _('Supplies subject to the reverse charge provision'), "amount": frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), "vat_amount": frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency') diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 37501a2638..847bda8271 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -52,7 +52,7 @@ def get_tax_accounts(company): ) if not tax_accounts_list and not frappe.flags.in_test: - frappe.throw(_(f'Please set Vat Accounts for Company: "{company}" in UAE VAT Settings')) + frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) for d in tax_accounts_list: for key, name in d.items(): tax_accounts_dict[name] = name From c70cc0d95080d3337b4613c56983e5fbaad47426 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Sep 2020 21:37:08 +0530 Subject: [PATCH 040/229] fix: tds calculation, skip invoices with "Apply Tax Withholding Amount" has disabled --- .../tax_withholding_category.py | 6 ++--- .../test_tax_withholding_category.py | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 8b5e68b359..32ad4cb03a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -140,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai else: tds_amount = _get_tds(net_total, tax_details.rate) else: - supplier_credit_amount = frappe.get_all('Purchase Invoice Item', - fields = ['sum(net_amount)'], - filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1) + supplier_credit_amount = frappe.get_all('Purchase Invoice', + fields = ['sum(net_total)'], + filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1) supplier_credit_amount = (supplier_credit_amount[0][0] if supplier_credit_amount and supplier_credit_amount[0][0] else 0) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index b1468999fc..a0b0cbb995 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS") + pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi.submit() + invoices.append(pi) + + # TDS not applied + pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True) + pi.submit() + invoices.append(pi) + + pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi.submit() + invoices.append(pi) + + self.assertEqual(pi.taxes_and_charges_deducted, 2000) + self.assertEqual(pi.grand_total, 8000) + + # delete invoices to avoid clashing + for d in invoices: + d.cancel() + def create_purchase_invoice(**args): # return sales invoice doc object item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) @@ -109,7 +132,7 @@ def create_purchase_invoice(**args): pi = frappe.get_doc({ "doctype": "Purchase Invoice", "posting_date": today(), - "apply_tds": 1, + "apply_tds": 0 if args.do_not_apply_tds else 1, "supplier": args.supplier, "company": '_Test Company', "taxes_and_charges": "", From c0e24735e362a64228c5e75421dc04ad78a2ef4d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 17 Oct 2020 22:31:36 +0530 Subject: [PATCH 041/229] feat: Add loan write off doctype --- .../doctype/loan_write_off/__init__.py | 0 .../doctype/loan_write_off/loan_write_off.js | 18 +++ .../loan_write_off/loan_write_off.json | 141 ++++++++++++++++++ .../doctype/loan_write_off/loan_write_off.py | 79 ++++++++++ .../loan_write_off/test_loan_write_off.py | 10 ++ 5 files changed, 248 insertions(+) create mode 100644 erpnext/loan_management/doctype/loan_write_off/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_write_off/loan_write_off.js create mode 100644 erpnext/loan_management/doctype/loan_write_off/loan_write_off.json create mode 100644 erpnext/loan_management/doctype/loan_write_off/loan_write_off.py create mode 100644 erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py diff --git a/erpnext/loan_management/doctype/loan_write_off/__init__.py b/erpnext/loan_management/doctype/loan_write_off/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js new file mode 100644 index 0000000000..cc5cd0d3a0 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js @@ -0,0 +1,18 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Write Off', { + refresh: function(frm) { + frm.set_query('write_off_account', function(){ + return { + filters: { + 'company': frm.doc.company, + 'root_type': 'Expense', + 'is_group': 0 + } + } + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json new file mode 100644 index 0000000000..64623c4b3a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json @@ -0,0 +1,141 @@ +{ + "actions": [], + "autoname": "LM-WO-.#####", + "creation": "2020-10-16 11:09:14.495066", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "applicant_type", + "applicant", + "column_break_3", + "company", + "posting_date", + "accounting_dimensions_section", + "cost_center", + "section_break_9", + "write_off_account", + "column_break_11", + "write_off_amount", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan", + "options": "Loan", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "loan.company", + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "read_only": 1 + }, + { + "fetch_from": "loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "label": "Applicant ", + "options": "applicant_type", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Write Off Details" + }, + { + "fieldname": "write_off_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Write Off Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "write_off_amount", + "fieldtype": "Currency", + "label": "Write Off Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Write Off", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2020-10-17 08:30:54.859362", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Write Off", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py new file mode 100644 index 0000000000..22fbe1ac57 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ +from frappe.utils import getdate +from frappe.model.document import Document +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries + +class LoanWriteOff(AccountsController): + def validate(self): + self.set_missing_values() + + def set_missing_values(self): + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.company) + + def on_submit(self): + self.update_outstanding_amount() + self.make_gl_entries() + + def on_cancel(self): + self.update_outstanding_amount(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] + self.make_gl_entries(cancel=1) + + def update_outstanding_amount(self, cancel=0): + written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount') + + if cancel: + written_off_amount -= self.write_off_amount + else: + written_off_amount += self.write_off_amount + + frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount) + + + def make_gl_entries(self, cancel=0): + gl_entries = [] + loan_details = frappe.get_doc("Loan", self.loan) + + gl_entries.append( + self.get_gl_dict({ + "account": self.write_off_account, + "against": loan_details.loan_account, + "debit": self.write_off_amount, + "debit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": self.write_off_account, + "credit": self.write_off_amount, + "credit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + make_gl_entries(gl_entries, cancel=cancel, merge_entries=False) + + diff --git a/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py new file mode 100644 index 0000000000..9f6700e274 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanWriteOff(unittest.TestCase): + pass From 9945ccc0cc76b08fe4f9e09fae134f066010a10b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Oct 2020 22:25:24 +0530 Subject: [PATCH 042/229] fix: Write Off amount handling in Loan accrual and closure --- .../loan_management/desk_page/loan/loan.json | 7 +-- erpnext/loan_management/doctype/loan/loan.js | 25 +++++++++- .../loan_management/doctype/loan/loan.json | 10 +++- erpnext/loan_management/doctype/loan/loan.py | 50 ++++++++++++++++--- .../doctype/loan/loan_dashboard.py | 2 +- .../loan_disbursement/loan_disbursement.json | 25 +++++++--- .../loan_interest_accrual.py | 9 ++-- .../doctype/loan_repayment/loan_repayment.py | 10 ++-- .../loan_security_unpledge.py | 6 +-- .../doctype/loan_type/loan_type.json | 17 ++++--- erpnext/loan_management/loan_common.js | 2 +- 11 files changed, 126 insertions(+), 37 deletions(-) diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index 3bdd1ce56e..fc59c19325 100644 --- a/erpnext/loan_management/desk_page/loan/loan.json +++ b/erpnext/loan_management/desk_page/loan/loan.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Loan", - "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n { \"dependencies\": [\n \"Loan Type\"\n ],\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Disbursement and Repayment", - "links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Write Off\",\n \"name\": \"Loan Write Off\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -34,10 +34,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Loan", - "modified": "2020-06-07 19:42:14.947902", + "modified": "2020-10-17 12:59:50.336085", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 0dc3bf8563..8d101b862a 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -64,12 +64,13 @@ frappe.ui.form.on('Loan', { frm.add_custom_button(__('Request Loan Closure'), function() { frm.trigger("request_loan_closure"); },__('Status')); + frm.add_custom_button(__('Loan Repayment'), function() { frm.trigger("make_repayment_entry"); },__('Create')); } - if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') { + if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) { frm.add_custom_button(__('Loan Disbursement'), function() { frm.trigger("make_loan_disbursement"); },__('Create')); @@ -80,6 +81,12 @@ frappe.ui.form.on('Loan', { frm.trigger("create_loan_security_unpledge"); },__('Create')); } + + if (["Loan Closure Requested", "Disbursed", "Partially Disbursed"].includes(frm.doc.status)) { + frm.add_custom_button(__('Loan Write Off'), function() { + frm.trigger("make_loan_write_off_entry"); + },__('Create')); + } } frm.trigger("toggle_fields"); }, @@ -130,6 +137,22 @@ frappe.ui.form.on('Loan', { }) }, + make_loan_write_off_entry: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name, + "company": frm.doc.company, + "as_dict": 1 + }, + method: "erpnext.loan_management.doctype.loan.loan.make_loan_write_off", + callback: function (r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + request_loan_closure: function(frm) { frappe.confirm(__("Do you really want to close this loan"), function() { diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index aa5e21b426..312e9affb9 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -43,6 +43,7 @@ "section_break_17", "total_payment", "total_principal_paid", + "written_off_amount", "column_break_19", "total_interest_payable", "total_amount_paid", @@ -330,11 +331,18 @@ "label": "Maximum Loan Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "written_off_amount", + "fieldtype": "Currency", + "label": "Written Off Amount", + "options": "Company:company:default_currency" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-01 12:36:11.255233", + "modified": "2020-10-17 10:35:44.361836", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 2d705fc296..8405d6ec62 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -143,7 +143,7 @@ class Loan(AccountsController): if pledge_list: frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET loan = '', status = 'Unpledged' - where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) + where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec def update_total_amount_paid(doc): total_amount_paid = 0 @@ -187,17 +187,22 @@ def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest return monthly_repayment_amount @frappe.whitelist() -def request_loan_closure(loan): - amounts = calculate_amounts(loan, getdate()) +def request_loan_closure(loan, posting_date=None): + if not posting_date: + posting_date = getdate() + amounts = calculate_amounts(loan, posting_date) pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest'] + loan_type = frappe.get_value('Loan', loan, 'loan_type') + write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') + # checking greater than 0 as there may be some minor precision error - if pending_amount > 0: - frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) - else: + if pending_amount < write_off_limit: # update status as loan closure requested frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + else: + frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) @frappe.whitelist() def get_loan_application(loan_application): @@ -217,6 +222,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo disbursement_entry.applicant = applicant disbursement_entry.company = company disbursement_entry.disbursement_date = nowdate() + disbursement_entry.posting_date = nowdate() disbursement_entry.disbursed_amount = pending_amount if as_dict: @@ -239,6 +245,38 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as else: return repayment_entry +@frappe.whitelist() +def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0): + if not company: + company = frappe.get_value('Loan', loan, 'company') + + if not posting_date: + posting_date = getdate() + + amounts = calculate_amounts(loan, posting_date) + pending_amount = amounts['pending_principal_amount'] + + if amount and (amount > pending_amount): + frappe.throw('Write Off amount cannot be greater than pending loan amount') + + if not amount: + amount = pending_amount + + # get default write off account from company master + write_off_account = frappe.get_value('Company', company, 'write_off_account') + + write_off = frappe.new_doc('Loan Write Off') + write_off.loan = loan + write_off.posting_date = posting_date + write_off.write_off_account = write_off_account + write_off.write_off_amount = amount + write_off.save() + + if as_dict: + return write_off.as_dict() + else: + return write_off + @frappe.whitelist() def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): # if loan is passed it will be considered as full unpledge diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py index 90d5ae2650..7a8190f745 100644 --- a/erpnext/loan_management/doctype/loan/loan_dashboard.py +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -13,7 +13,7 @@ def get_data(): 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement'] }, { - 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge'] + 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge'] } ] } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index c437a987eb..89f671bcc0 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -26,19 +26,23 @@ { "fieldname": "against_loan", "fieldtype": "Link", + "in_list_view": 1, "label": "Against Loan ", - "options": "Loan" + "options": "Loan", + "reqd": 1 }, { "fieldname": "disbursement_date", "fieldtype": "Date", - "label": "Disbursement Date" + "label": "Disbursement Date", + "reqd": 1 }, { "fieldname": "disbursed_amount", "fieldtype": "Currency", "label": "Disbursed Amount", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "reqd": 1 }, { "fieldname": "amended_from", @@ -53,17 +57,21 @@ "fetch_from": "against_loan.company", "fieldname": "company", "fieldtype": "Link", + "in_list_view": 1, "label": "Company", "options": "Company", - "read_only": 1 + "read_only": 1, + "reqd": 1 }, { "fetch_from": "against_loan.applicant", "fieldname": "applicant", "fieldtype": "Dynamic Link", + "in_list_view": 1, "label": "Applicant", "options": "applicant_type", - "read_only": 1 + "read_only": 1, + "reqd": 1 }, { "collapsible": 1, @@ -102,9 +110,11 @@ "fetch_from": "against_loan.applicant_type", "fieldname": "applicant_type", "fieldtype": "Select", + "in_list_view": 1, "label": "Applicant Type", "options": "Employee\nMember\nCustomer", - "read_only": 1 + "read_only": 1, + "reqd": 1 }, { "fieldname": "bank_account", @@ -117,9 +127,10 @@ "fieldtype": "Column Break" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-29 05:20:41.629911", + "modified": "2020-10-16 10:04:26.229216", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 4517de0c59..e31b844953 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -89,9 +89,10 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if loan.status == 'Disbursed': pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) else: - pending_principal_amount = loan.disbursed_amount + pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) payable_interest = interest_per_day * no_of_days @@ -128,7 +129,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte open_loans = frappe.get_all("Loan", fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", - "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], + "rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"], filters=query_filters) for loan in open_loans: @@ -239,5 +240,5 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): precision = cint(frappe.db.get_default("currency_precision")) or 2 - return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), 2) + return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 940f82ee34..de5ba8fcd5 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -31,8 +31,8 @@ class LoanRepayment(AccountsController): def on_cancel(self): self.mark_as_unpaid() - self.make_gl_entries(cancel=1) self.ignore_linked_doctypes = ['GL Entry'] + self.make_gl_entries(cancel=1) def set_missing_values(self, amounts): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -235,7 +235,7 @@ class LoanRepayment(AccountsController): "against": loan_details.loan_account + ", " + loan_details.interest_income_account + ", " + loan_details.penalty_income_account, "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid , + "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, "remarks": _("Against Loan:") + self.against_loan, @@ -344,9 +344,11 @@ def get_amounts(amounts, against_loan, posting_date): final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): - pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \ + - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount else: - pending_principal_amount = against_loan_doc.disbursed_amount + pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \ + - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount unaccrued_interest = 0 if due_date: diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index b3eb6001e4..d0d25e8897 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -42,10 +42,10 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', - 'total_interest_payable']) + total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable', 'written_off_amount']) - pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) security_value = 0 for security in self.securities: diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 669490a448..5d9232d711 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -11,6 +11,7 @@ "rate_of_interest", "penalty_interest_rate", "grace_period_in_days", + "write_off_amount", "column_break_2", "company", "is_term_loan", @@ -76,7 +77,6 @@ "reqd": 1 }, { - "description": "This account is used for booking loan repayments from the borrower and also disbursing loans to the borrower", "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", @@ -84,7 +84,6 @@ "reqd": 1 }, { - "description": "This account is capital account which is used to allocate capital for loan disbursal account ", "fieldname": "loan_account", "fieldtype": "Link", "label": "Loan Account", @@ -96,7 +95,6 @@ "fieldtype": "Column Break" }, { - "description": "This account will be used for booking loan interest accruals", "fieldname": "interest_income_account", "fieldtype": "Link", "label": "Interest Income Account", @@ -104,7 +102,6 @@ "reqd": 1 }, { - "description": "This account will be used for booking penalties levied due to delayed repayments", "fieldname": "penalty_income_account", "fieldtype": "Link", "label": "Penalty Income Account", @@ -113,7 +110,6 @@ }, { "default": "0", - "description": "If this is not checked the loan by default will be considered as a Demand Loan", "fieldname": "is_term_loan", "fieldtype": "Check", "label": "Is Term Loan" @@ -145,11 +141,20 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "allow_on_submit": 1, + "description": "Pending amount that will be automatically ignored on loan closure request ", + "fieldname": "write_off_amount", + "fieldtype": "Currency", + "label": "Write Off Amount ", + "options": "Company:company:default_currency" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-06-07 18:55:59.346292", + "modified": "2020-10-17 11:41:17.907683", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js index 33a5de0566..50b68da30e 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -8,7 +8,7 @@ frappe.ui.form.on(cur_frm.doctype, { frm.refresh_field('applicant_type'); } - if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype) + if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off'].includes(frm.doc.doctype) && frm.doc.docstatus > 0) { frm.add_custom_button(__("Accounting Ledger"), function() { From 8cd8dbe15d736d33d04106def46c586bf39712b3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Oct 2020 22:26:09 +0530 Subject: [PATCH 043/229] fix: Add write off test --- .../loan_management/doctype/loan/test_loan.py | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 7b653652ea..c38541196f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure +from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure, make_loan_write_off from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount @@ -496,6 +496,96 @@ class TestLoan(unittest.TestCase): self.assertEquals(calculated_penalty_amount, penalty_amount) + def test_loan_write_off_limit(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + # repay 50 less so that it can be automatically written off + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + flt(loan.loan_amount + accrued_interest_amount - 50)) + + repayment_entry.submit() + + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + + self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50) + + request_loan_closure(loan.name) + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + def test_loan_amount_write_off(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + # repay 100 less so that it can be automatically written off + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + flt(loan.loan_amount + accrued_interest_amount - 100)) + + repayment_entry.submit() + + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + + self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100) + + we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount']) + we.submit() + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): @@ -579,7 +669,8 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i "interest_income_account": interest_income_account, "penalty_income_account": penalty_income_account, "repayment_method": repayment_method, - "repayment_periods": repayment_periods + "repayment_periods": repayment_periods, + "write_off_amount": 100 }).insert() loan_type.submit() From 32eb9a78930d2771f68ee25ad1930bda5929545f Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 19 Oct 2020 17:58:50 +0530 Subject: [PATCH 044/229] feat(UAE VAT 201): claimable to recoverable --- erpnext/regional/united_arab_emirates/setup.py | 10 +++++----- erpnext/regional/united_arab_emirates/utils.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index a0e7620919..efa39bd3c5 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -38,13 +38,13 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Read Only', insert_after='supplier_name', fetch_from='supplier.supplier_name_in_arabic', print_hide=1), - dict(fieldname='claimable_standard_rated_expenses', label='Claimable Standard Rated Expenses (AED)', + dict(fieldname='recoverable_standard_rated_expenses', label='Recoverable Standard Rated Expenses (AED)', insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0', depends_on="eval:doc.reverse_charge=='N'",), dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='claimable_standard_rated_expenses', print_hide=1, + fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1, options='Y\nN', default='N'), - dict(fieldname='claimable_reverse_charge', label='Claimable Reverse Charge (Percentage)', + dict(fieldname='recoverable_reverse_charge', label='Recoverable Reverse Charge (Percentage)', insert_after='reverse_charge', fieldtype='Percent', print_hide=1, depends_on="eval:doc.reverse_charge=='Y'", default='100.000'), ] @@ -57,7 +57,7 @@ def make_custom_fields(): fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), dict(fieldname='emirate', label='Emirate', insert_after='customer_address', - fieldtype='Read Only', fetch_from='customer_address.emirates'), + fieldtype='Read Only', fetch_from='customer_address.emirate'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), ] @@ -102,7 +102,7 @@ def make_custom_fields(): fieldtype='Data', insert_after='supplier_name'), ], 'Address': [ - dict(fieldname='emirates', label='Emirates', fieldtype='Select', insert_after='state', + dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', options='Abu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 847bda8271..7edc2b6720 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -147,5 +147,5 @@ def validate_returns(doc, method): country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'United Arab Emirates': return - if doc.reverse_charge == 'Y' and flt(doc.claimable_standard_rated_expenses) != 0: - frappe.throw(_("Claimable Standard Rated expenses should not be set when Reverse Charge Applicable is Y")) \ No newline at end of file + if doc.reverse_charge == 'Y' and flt(doc.recoverable_standard_rated_expenses) != 0: + frappe.throw(_("Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y")) \ No newline at end of file From c1b80047571a579c34899ad4e458c28e89bf4c31 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 19 Oct 2020 20:47:56 +0530 Subject: [PATCH 045/229] feat(UAE VAT 21): claimable to recoverable --- .../regional/report/uae_vat_201/uae_vat_201.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index a2518c38c6..b5ff8389e3 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -245,14 +245,14 @@ def get_conditions_join(filters): return conditions def get_reverse_charge_recoverable_total(filters): - """Returns the sum of the total of each Purchase invoice made with claimable reverse charge.""" + """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(total) from `tabPurchase Invoice` where reverse_charge = "Y" - and claimable_reverse_charge > 0 + and recoverable_reverse_charge > 0 and docstatus = 1 {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 @@ -260,26 +260,26 @@ def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" - select sum(debit * `tabPurchase Invoice`.claimable_reverse_charge / 100) from + select sum(debit * `tabPurchase Invoice`.recoverable_reverse_charge / 100) from `tabPurchase Invoice` inner join `tabGL Entry` on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name where `tabPurchase Invoice`.reverse_charge = "Y" and `tabPurchase Invoice`.docstatus = 1 - and `tabPurchase Invoice`.claimable_reverse_charge > 0 + and `tabPurchase Invoice`.recoverable_reverse_charge > 0 and `tabGL Entry`.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 def get_standard_rated_expenses_total(filters): - """Returns the sum of the total of each Purchase invoice made with claimable reverse charge.""" + """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" conditions = get_conditions(filters) return frappe.db.sql(""" select sum(total) from `tabPurchase Invoice` where - claimable_standard_rated_expenses > 0 + recoverable_standard_rated_expenses > 0 and docstatus = 1 {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 @@ -287,10 +287,10 @@ def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions(filters) return frappe.db.sql(""" - select sum(claimable_standard_rated_expenses) from + select sum(recoverable_standard_rated_expenses) from `tabPurchase Invoice` where - claimable_standard_rated_expenses > 0 + recoverable_standard_rated_expenses > 0 and docstatus = 1 {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 From 33cdfdfb7f21661997cef6fa70095f436b348054 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 19 Oct 2020 20:48:43 +0530 Subject: [PATCH 046/229] feat(UAE VAT 201): Add tests for report --- .../purchase_invoice/test_purchase_invoice.py | 3 +- .../report/uae_vat_201/test_uae_vat_201.py | 370 ++++++++++++++++++ 2 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 erpnext/regional/report/uae_vat_201/test_uae_vat_201.py diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2e5a7142a3..8276c1fc03 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1040,7 +1040,8 @@ def make_purchase_invoice_against_cost_center(**args): pi.is_return = args.is_return pi.credit_to = args.return_against or "Creditors - _TC" pi.is_subcontracted = args.is_subcontracted or "No" - pi.supplier_warehouse = "_Test Warehouse 1 - _TC" + if args.supplier_warehouse: + pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.append("items", { "item_code": args.item or args.item_code or "_Test Item", diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py new file mode 100644 index 0000000000..8e845d7432 --- /dev/null +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -0,0 +1,370 @@ +# coding=utf-8 +from __future__ import unicode_literals + +import erpnext +import frappe +from unittest import TestCase +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse_account +from erpnext.regional.report.uae_vat_201.uae_vat_201 import ( + get_total_emiratewise, + get_tourist_tax_return_total, + get_tourist_tax_return_tax, + get_reverse_charge_total, + get_reverse_charge_tax, + get_zero_rated_total, + get_exempt_total, + get_standard_rated_expenses_total, + get_standard_rated_expenses_tax, + get_reverse_charge_recoverable_total, + get_reverse_charge_recoverable_tax + ) + + + +class TestUaeVat201(TestCase): + def setUp(self): + frappe.set_user("Administrator") + + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company UAE VAT'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company UAE VAT'") + + + make_company("_Test Company UAE VAT", "_TCUV") + set_vat_accounts() + + make_customers() + + make_supplier() + + create_warehouse("_Test UAE VAT Supplier Warehouse", company="_Test Company UAE VAT") + + + make_item("_Test UAE VAT Item", properties = {"is_zero_rated": 0, "is_exempt": 0}) + make_item("_Test UAE VAT Zero Rated Item", properties = {"is_zero_rated": 1, "is_exempt": 0}) + make_item("_Test UAE VAT Exempt Item", properties = {"is_zero_rated": 0, "is_exempt": 1}) + + make_sales_invoices() + + create_purchase_invoices() + + def test_uae_vat_201_report(self): + filters = {"company": "_Test Company UAE VAT"} + total_emiratewise = get_total_emiratewise(filters) + amounts_by_emirate = {} + for d in total_emiratewise: + emirate, amount, vat= d + amounts_by_emirate[emirate] = { + "raw_amount": amount, + "raw_vat_amount": vat, + } + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],300) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) + + self.assertEqual(get_tourist_tax_return_tax(filters),2) + self.assertEqual(get_reverse_charge_total(filters),250) + self.assertEqual(get_reverse_charge_tax(filters),12.5) + self.assertEqual(get_zero_rated_total(filters),100) + self.assertEqual(get_exempt_total(filters),100) + self.assertEqual(get_standard_rated_expenses_total(filters),250) + self.assertEqual(get_standard_rated_expenses_tax(filters),1) + self.assertEqual(get_reverse_charge_recoverable_total(filters),250) + self.assertEqual(get_reverse_charge_recoverable_tax(filters),12.5) + +def make_company(company_name, abbr): + if not frappe.db.exists("Company", company_name): + company = frappe.get_doc({ + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "AED", + "country": "United Arab Emirates", + "create_chart_of_accounts_based_on": "Standard Template", + }) + company.insert() + else: + company = frappe.get_doc("Company", company_name) + + # indempotent + company.create_default_warehouses() + + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): + company.create_default_cost_center() + + company.save() + return company + +def set_vat_accounts(): + if not frappe.db.exists("UAE VAT Settings", "_Test Company UAE VAT"): + vat_accounts = frappe.get_all( + "Account", + fields=["name"], + filters = { + "company": "_Test Company UAE VAT", + "is_group":0, + "account_type": "Tax"}) + + uae_vat_accounts = [] + for d in vat_accounts: + uae_vat_accounts.append( + { + "doctype": "UAE VAT Account", + "account":d.name + }) + + frappe.get_doc({ + "company": "_Test Company UAE VAT", + "uae_vat_accounts": uae_vat_accounts, + "doctype": "UAE VAT Settings", + }).insert() + +def make_customers(): + if not frappe.db.exists("Customer", "_Test Dubai Customer"): + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": "_Test Dubai Customer", + "customer_type": "Company", + }) + customer.insert() + else: + customer = frappe.get_doc("Customer", "_Test Dubai Customer") + + if not frappe.db.exists("Customer", "_Test Sharjah Customer"): + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": "_Test Sharjah Customer", + "customer_type": "Company", + }) + customer.insert() + else: + customer = frappe.get_doc("Customer", "_Test Sharjah Customer") + + if not frappe.db.exists('Address', '_Test Dubai Address'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Dubai Address", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "United Arab Emirates", + "doctype": "Address", + "emirate": "Dubai" + }).insert() + + address.append("links", { + "link_doctype": "Customer", + "link_name": "_Test Dubai Customer" + }) + + address.save() + + if not frappe.db.exists('Address', '_Test Sharjah Address'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Sharjah Address", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "United Arab Emirates", + "doctype": "Address", + "emirate": "Sharjah" + }).insert() + + address.append("links", { + "link_doctype": "Customer", + "link_name": "_Test Sharjah Customer" + }) + + address.save() + +def make_supplier(): + + if not frappe.db.exists("Supplier", "_Test UAE Supplier"): + frappe.get_doc({ + "supplier_group": "Local", + "supplier_name": "_Test UAE Supplier", + "supplier_type": "Individual", + "doctype": "Supplier", + }).insert() + +def create_warehouse(warehouse_name, properties=None, company=None): + if not company: + company = "_Test Company" + + warehouse_id = erpnext.encode_company_abbr(warehouse_name, company) + if not frappe.db.exists("Warehouse", warehouse_id): + w = frappe.new_doc("Warehouse") + w.warehouse_name = warehouse_name + w.parent_warehouse = "All Warehouses - _TCUV" + w.company = company + w.account = get_warehouse_account(warehouse_name, company) + if properties: + w.update(properties) + w.save() + return w.name + else: + return warehouse_id + +def make_item(item_code, properties=None): + if frappe.db.exists("Item", item_code): + return frappe.get_doc("Item", item_code) + + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products" + }) + + if properties: + item.update(properties) + + item.insert() + + return item + +def make_sales_invoices(): + si = create_sales_invoice(company="_Test Company UAE VAT", + customer = '_Test Dubai Customer', + currency = 'AED', + warehouse = 'Finished Goods - _TCUV', + debit_to = 'Debtors - _TCUV', + income_account = 'Sales - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + cost_center = 'Main - _TCUV', + sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", + item = "_Test UAE VAT Item", + do_not_save=1 + ) + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + }) + si.submit() + + si = create_sales_invoice(company="_Test Company UAE VAT", + customer = '_Test Sharjah Customer', + currency = 'AED', + warehouse = 'Finished Goods - _TCUV', + debit_to = 'Debtors - _TCUV', + income_account = 'Sales - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + cost_center = 'Main - _TCUV', + sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", + item = "_Test UAE VAT Item", + do_not_save=1 + ) + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + }) + si.submit() + + si = create_sales_invoice(company="_Test Company UAE VAT", + customer = '_Test Dubai Customer', + currency = 'AED', + warehouse = 'Finished Goods - _TCUV', + debit_to = 'Debtors - _TCUV', + income_account = 'Sales - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + cost_center = 'Main - _TCUV', + sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", + item = "_Test UAE VAT Item", + do_not_save=1 + ) + + si.tourist_tax_return = 2 + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + }) + si.submit() + + si = create_sales_invoice(company="_Test Company UAE VAT", + customer = '_Test Sharjah Customer', + currency = 'AED', + warehouse = 'Finished Goods - _TCUV', + debit_to = 'Debtors - _TCUV', + income_account = 'Sales - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + cost_center = 'Main - _TCUV', + sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", + item = "_Test UAE VAT Zero Rated Item", + ) + + si = create_sales_invoice(company="_Test Company UAE VAT", + customer = '_Test Sharjah Customer', + currency = 'AED', + warehouse = 'Finished Goods - _TCUV', + debit_to = 'Debtors - _TCUV', + income_account = 'Sales - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + cost_center = 'Main - _TCUV', + sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", + item = "_Test UAE VAT Exempt Item", + ) + +def create_purchase_invoices(): + + pi = make_purchase_invoice( + company="_Test Company UAE VAT", + supplier = '_Test UAE Supplier', + warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', + currency = 'AED', + cost_center = 'Main - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + item = "_Test UAE VAT Item", + do_not_save=1, + ) + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + }) + + pi.recoverable_standard_rated_expenses = 1 + + pi.submit() + + pi = make_purchase_invoice( + company="_Test Company UAE VAT", + supplier = '_Test UAE Supplier', + warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', + currency = 'AED', + cost_center = 'Main - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + item = "_Test UAE VAT Item", + do_not_save=1, + ) + + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + }) + + pi.reverse_charge = "Y" + + pi.recoverable_reverse_charge = 100 + + pi.submit() + + From 853fd03f4910c9bd8641085ba876152e11f7728b Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 19 Oct 2020 22:35:16 +0530 Subject: [PATCH 047/229] feat(uae vat 201): fix linter issues --- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 8e845d7432..6a255b8a0b 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -64,6 +64,7 @@ class TestUaeVat201(TestCase): self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) + self.assertEqual(get_tourist_tax_return_total(filters),100) self.assertEqual(get_tourist_tax_return_tax(filters),2) self.assertEqual(get_reverse_charge_total(filters),250) self.assertEqual(get_reverse_charge_tax(filters),12.5) From 234036df32fc6db164cf930dbc1e37dfabf1fee6 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Tue, 20 Oct 2020 07:42:10 +0530 Subject: [PATCH 048/229] chore(uae vat 201): solve travis issues --- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 6a255b8a0b..648d98e204 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -341,6 +341,7 @@ def create_purchase_invoices(): pi.recoverable_standard_rated_expenses = 1 + pi.insert() pi.submit() pi = make_purchase_invoice( @@ -366,6 +367,7 @@ def create_purchase_invoices(): pi.recoverable_reverse_charge = 100 + pi.insert() pi.submit() From c08591124c172da56a2e917644d864f4c6dc0ef5 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Tue, 20 Oct 2020 10:17:48 +0530 Subject: [PATCH 049/229] chore(UAE VAT 21): solve merge conflicts --- .../doctype/purchase_invoice/test_purchase_invoice.py | 2 +- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 8276c1fc03..f2499d24b5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -998,7 +998,7 @@ def make_purchase_invoice(**args): 'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC', "conversion_factor": 1.0, "serial_no": args.serial_no, - "stock_uom": "_Test UOM", + "stock_uom": args.uom or "_Test UOM", "cost_center": args.cost_center or "_Test Cost Center - _TC", "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 648d98e204..68c163d5bc 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -324,12 +324,14 @@ def create_purchase_invoices(): pi = make_purchase_invoice( company="_Test Company UAE VAT", supplier = '_Test UAE Supplier', + supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', currency = 'AED', cost_center = 'Main - _TCUV', expense_account = 'Cost of Goods Sold - _TCUV', item = "_Test UAE VAT Item", do_not_save=1, + uom = "Nos" ) pi.append("taxes", { "charge_type": "On Net Total", @@ -341,18 +343,19 @@ def create_purchase_invoices(): pi.recoverable_standard_rated_expenses = 1 - pi.insert() pi.submit() pi = make_purchase_invoice( company="_Test Company UAE VAT", supplier = '_Test UAE Supplier', + supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', currency = 'AED', cost_center = 'Main - _TCUV', expense_account = 'Cost of Goods Sold - _TCUV', item = "_Test UAE VAT Item", do_not_save=1, + uom = "Nos" ) pi.append("taxes", { @@ -367,7 +370,6 @@ def create_purchase_invoices(): pi.recoverable_reverse_charge = 100 - pi.insert() pi.submit() From 6a24da4efd860b9f32ea61bb3cfc7ab23d787909 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Tue, 20 Oct 2020 11:53:06 +0530 Subject: [PATCH 050/229] refactor(UAE VAT 201): break functions --- .../report/uae_vat_201/uae_vat_201.py | 172 +++++++----------- 1 file changed, 69 insertions(+), 103 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index b5ff8389e3..90e7501291 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -46,109 +46,8 @@ def get_columns(): def get_data(filters = None): """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data.""" data = [] - data.append({ - "no": '', - "legend": _('VAT on Sales and All Other Outputs'), - "amount": '', - "vat_amount": '' - }) - - total_emiratewise = get_total_emiratewise(filters) - emirates = get_emirates() - amounts_by_emirate = {} - for d in total_emiratewise: - emirate, amount, vat= d - amounts_by_emirate[emirate] = { - "legend": emirate, - "raw_amount": amount, - "raw_vat_amount": vat, - "amount": frappe.format(amount, 'Currency'), - "vat_amount": frappe.format(vat, 'Currency'), - } - - for d, emirate in enumerate(emirates, 97): - if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(d)) - amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) - data.append(amounts_by_emirate[emirate]) - else: - data.append( - { - "no": _('1{0}').format(chr(d)), - "legend": _('Standard rated supplies in {0}').format(emirate), - "amount": frappe.format(0, 'Currency'), - "vat_amount": frappe.format(0, 'Currency') - } - ) - - data.append( - { - "no": '2', - "legend": _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), - "amount": frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), - "vat_amount": frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency') - } - ) - - data.append( - { - "no": '3', - "legend": _('Supplies subject to the reverse charge provision'), - "amount": frappe.format(get_reverse_charge_total(filters), 'Currency'), - "vat_amount": frappe.format(get_reverse_charge_tax(filters), 'Currency') - } - ) - - data.append( - { - "no": '4', - "legend": _('Zero Rated'), - "amount": frappe.format(get_zero_rated_total(filters), 'Currency'), - "vat_amount": "-" - } - ) - - data.append( - { - "no": '5', - "legend": _('Exempt Supplies'), - "amount": frappe.format(get_exempt_total(filters), 'Currency'), - "vat_amount": "-" - } - ) - - data.append({ - "no": '', - "legend": '', - "amount": '', - "vat_amount": '' - }) - - data.append({ - "no": '', - "legend": _('VAT on Expenses and All Other Inputs'), - "amount": '', - "vat_amount": '' - }) - - data.append( - { - "no": '9', - "legend": _('Standard Rated Expenses'), - "amount": frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), - "vat_amount": frappe.format(get_standard_rated_expenses_tax(filters), 'Currency') - } - ) - - data.append( - { - "no": '10', - "legend": _('Supplies subject to the reverse charge provision'), - "amount": frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), - "vat_amount": frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency') - } - ) - + emirates, amounts_by_emirate = append_vat_on_sales(data, filters) + append_vat_on_expenses(data, filters) return data, emirates, amounts_by_emirate @@ -178,6 +77,73 @@ def get_chart(emirates, amounts_by_emirate): chart["fieldtype"] = "Currency" return chart +def append_vat_on_sales(data, filters): + """Appends Sales and All Other Outputs""" + append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '') + + emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) + + append_data(data, '2', _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), + frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), + frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency')) + + append_data(data, '3', _('Supplies subject to the reverse charge provision'), + frappe.format(get_reverse_charge_total(filters), 'Currency'), + frappe.format(get_reverse_charge_tax(filters), 'Currency')) + + append_data(data, '4', _('Zero Rated'), + frappe.format(get_zero_rated_total(filters), 'Currency'), "-") + + append_data(data, '5', _('Exempt Supplies'), + frappe.format(get_exempt_total(filters), 'Currency'),"-") + + append_data(data, '', '', '', '') + + return emirates, amounts_by_emirate + +def standard_rated_expenses_emiratewise(data, filters): + """"Append emiratewise standard rated expenses and vat""" + total_emiratewise = get_total_emiratewise(filters) + emirates = get_emirates() + amounts_by_emirate = {} + for d in total_emiratewise: + emirate, amount, vat= d + amounts_by_emirate[emirate] = { + "legend": emirate, + "raw_amount": amount, + "raw_vat_amount": vat, + "amount": frappe.format(amount, 'Currency'), + "vat_amount": frappe.format(vat, 'Currency'), + } + + for d, emirate in enumerate(emirates, 97): + if emirate in amounts_by_emirate: + amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(d)) + amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) + data.append(amounts_by_emirate[emirate]) + else: + append_data(data, _('1{0}').format(chr(d)), + _('Standard rated supplies in {0}').format(emirate), + frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) + return emirates, amounts_by_emirate + + +def append_vat_on_expenses(data, filters): + """Appends Expenses and All Other Inputs""" + append_data(data, '', _('VAT on Expenses and All Other Inputs'), '', '') + append_data(data, '9', _('Standard Rated Expenses'), + frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), + frappe.format(get_standard_rated_expenses_tax(filters), 'Currency')) + + append_data(data, '10', _('Supplies subject to the reverse charge provision'), + frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), + frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency') +) + +def append_data(data, no, legend, amount, vat_amount): + """Returns data with appended value.""" + data.append({"no": no, "legend":legend, "amount": amount, "vat_amount": vat_amount}) + def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" return frappe.db.sql(f""" From 33e9a1ab31f5d22c2a865f84bdc8aa931c071545 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Tue, 20 Oct 2020 13:04:38 +0530 Subject: [PATCH 051/229] refactor(UAE VAT 201): solve linting issues --- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 90e7501291..82032461ce 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -78,7 +78,7 @@ def get_chart(emirates, amounts_by_emirate): return chart def append_vat_on_sales(data, filters): - """Appends Sales and All Other Outputs""" + """Appends Sales and All Other Outputs.""" append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '') emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) @@ -102,7 +102,7 @@ def append_vat_on_sales(data, filters): return emirates, amounts_by_emirate def standard_rated_expenses_emiratewise(data, filters): - """"Append emiratewise standard rated expenses and vat""" + """Append emiratewise standard rated expenses and vat""" total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() amounts_by_emirate = {} @@ -129,7 +129,7 @@ def standard_rated_expenses_emiratewise(data, filters): def append_vat_on_expenses(data, filters): - """Appends Expenses and All Other Inputs""" + """Appends Expenses and All Other Inputs.""" append_data(data, '', _('VAT on Expenses and All Other Inputs'), '', '') append_data(data, '9', _('Standard Rated Expenses'), frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), From 8796567b4207195a4b24353470595e39ce1e56fd Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Tue, 20 Oct 2020 16:35:28 +0530 Subject: [PATCH 052/229] chore(UAE VAT 201): add puctuation --- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 2 +- erpnext/regional/united_arab_emirates/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 82032461ce..5bed0e2463 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -102,7 +102,7 @@ def append_vat_on_sales(data, filters): return emirates, amounts_by_emirate def standard_rated_expenses_emiratewise(data, filters): - """Append emiratewise standard rated expenses and vat""" + """Append emiratewise standard rated expenses and vat.""" total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() amounts_by_emirate = {} diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index efa39bd3c5..fac6e270db 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -89,7 +89,7 @@ def make_custom_fields(): dict(fieldname='is_zero_rated', label='Is Zero Rated', fieldtype='Check', insert_after='tax_code', print_hide=1), - dict(fieldname='is_exempt', label='Is Exempt ', + dict(fieldname='is_exempt', label='Is Exempt', fieldtype='Check', insert_after='is_zero_rated', print_hide=1) ], From 2f65ab5355a86e3422b9fe81142d7aa84d24ccc9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 10:40:59 +0530 Subject: [PATCH 053/229] fix: Validatiion for loan write off amountt --- .../doctype/loan_write_off/loan_write_off.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 22fbe1ac57..823e6a904f 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -5,19 +5,28 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import getdate -from frappe.model.document import Document +from frappe.utils import getdate, flt from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries class LoanWriteOff(AccountsController): def validate(self): self.set_missing_values() + self.validate_write_off_amount() def set_missing_values(self): if not self.cost_center: self.cost_center = erpnext.get_default_cost_center(self.company) + def validate_write_off_amount(self): + total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, + ['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount']) + + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) + + if self.write_off_amount > pending_principal_amount: + frappe.throw(_("Write off amount cannot be greater than pending principal amount")) + def on_submit(self): self.update_outstanding_amount() self.make_gl_entries() From 13cbda110d46bb171dc0162dd8e3bafb8f754f66 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 16:11:52 +0530 Subject: [PATCH 054/229] fix: Test Case --- .../doctype/loan_interest_accrual/test_loan_interest_accrual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 4b85b21869..5495d1c70d 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -57,4 +57,4 @@ class TestLoanInterestAccrual(unittest.TestCase): loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) - self.assertEquals(flt(loan_interest_accural.interest_amount, 2), flt(accrued_interest_amount, 2)) + self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0)) From 8f9bab79a7128671191111cb4dfea95c5e588c5e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 19:03:13 +0530 Subject: [PATCH 055/229] fix: Update no copy fields --- erpnext/loan_management/doctype/loan/loan.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 312e9affb9..8a3b2fa029 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -76,6 +76,7 @@ "fieldname": "loan_application", "fieldtype": "Link", "label": "Loan Application", + "no_copy": 1, "options": "Loan Application" }, { @@ -149,7 +150,8 @@ "depends_on": "eval:doc.status==\"Disbursed\"", "fieldname": "disbursement_date", "fieldtype": "Date", - "label": "Disbursement Date" + "label": "Disbursement Date", + "no_copy": 1 }, { "depends_on": "is_term_loan", @@ -253,6 +255,7 @@ "fieldname": "total_payment", "fieldtype": "Currency", "label": "Total Payable Amount", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -266,6 +269,7 @@ "fieldname": "total_interest_payable", "fieldtype": "Currency", "label": "Total Interest Payable", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -274,6 +278,7 @@ "fieldname": "total_amount_paid", "fieldtype": "Currency", "label": "Total Amount Paid", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -314,6 +319,7 @@ "fieldname": "total_principal_paid", "fieldtype": "Currency", "label": "Total Principal Paid", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -321,6 +327,7 @@ "fieldname": "disbursed_amount", "fieldtype": "Currency", "label": "Disbursed Amount", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -329,6 +336,7 @@ "fieldname": "maximum_loan_amount", "fieldtype": "Currency", "label": "Maximum Loan Amount", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -336,13 +344,14 @@ "fieldname": "written_off_amount", "fieldtype": "Currency", "label": "Written Off Amount", + "no_copy": 1, "options": "Company:company:default_currency" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-17 10:35:44.361836", + "modified": "2020-10-21 09:12:26.809228", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", From 3f177bffac645cb9218e4799835806b41487cdb9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 22:15:18 +0530 Subject: [PATCH 056/229] fix: Add test for loan top up --- .../loan_management/doctype/loan/test_loan.py | 10 +++-- .../test_loan_disbursement.py | 44 ++++++++++++++++++- .../test_loan_interest_accrual.py | 2 +- .../doctype/loan_repayment/loan_repayment.py | 8 ++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index c38541196f..1634697939 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -320,7 +320,7 @@ class TestLoan(unittest.TestCase): amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6)) self.assertTrue(amounts['pending_principal_amount'] < 0) - self.assertTrue(amounts['payable_principal_amount'] < 0) + self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) def test_disbursal_check_with_shortfall(self): @@ -706,7 +706,7 @@ def create_loan_security(): "haircut": 50.00, }).insert(ignore_permissions=True) -def create_loan_security_pledge(applicant, pledges, loan_application): +def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None): lsp = frappe.new_doc("Loan Security Pledge") lsp.applicant_type = 'Customer' @@ -714,11 +714,13 @@ def create_loan_security_pledge(applicant, pledges, loan_application): lsp.company = "_Test Company" lsp.loan_application = loan_application + if loan: + lsp.loan = loan + for pledge in pledges: lsp.append('securities', { "loan_security": pledge['loan_security'], - "qty": pledge['qty'], - "haircut": pledge['haircut'] + "qty": pledge['qty'] }) lsp.save() diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 3ade5c549b..aaaeea8c4e 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -8,9 +8,10 @@ from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_la from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application, make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price) from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans -from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year, get_per_day_interest from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoanDisbursement(unittest.TestCase): @@ -68,3 +69,44 @@ class TestLoanDisbursement(unittest.TestCase): # After repayment loan disbursement entry should go through make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) + def test_loan_topup_with_additional_pledge(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + # Disbursed 10,00,000 amount + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + amounts = calculate_amounts(loan.name, add_days(last_date, 1)) + + previous_interest = amounts['interest_amount'] + + pledge1 = [{ + "loan_security": "Test Security 1", + "qty": 2000.00 + }] + + create_loan_security_pledge(self.applicant, pledge1, loan=loan.name) + + # Topup 500000 + make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1)) + process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15)) + amounts = calculate_amounts(loan.name, add_days(last_date, 15)) + + per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30') + interest = per_day_interest * 15 + + self.assertEquals(amounts['pending_principal_amount'], 1500000) + self.assertEquals(amounts['interest_amount'], flt(interest + previous_interest, 2)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 5495d1c70d..46a6440553 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) -from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price, +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_price, make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application) from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index de5ba8fcd5..6b3fba41c8 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -357,8 +357,8 @@ def get_amounts(amounts, against_loan, posting_date): pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 if pending_days > 0: - payable_principal_amount = flt(pending_principal_amount, precision) - per_day_interest = get_per_day_interest(payable_principal_amount, loan_type_details.rate_of_interest, posting_date) + principal_amount = flt(pending_principal_amount, precision) + per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date) unaccrued_interest += (pending_days * flt(per_day_interest, precision)) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) @@ -389,9 +389,11 @@ def calculate_amounts(against_loan, posting_date, payment_type=''): amounts = get_amounts(amounts, against_loan, posting_date) + # update values for closure if payment_type == 'Loan Closure': - amounts['payable_amount'] += amounts['unaccrued_interest'] + amounts['payable_principal_amount'] = amounts['pending_principal_amount'] amounts['interest_amount'] += amounts['unaccrued_interest'] + amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] return amounts From 2c97244bada37ce793c466ed9bfef75910f5fda7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 22 Oct 2020 09:34:27 +0530 Subject: [PATCH 057/229] fix: Test Cases --- .../doctype/loan_security_pledge/loan_security_pledge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index 2bb6fd84e5..cbc8376aa5 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -78,7 +78,7 @@ class LoanSecurityPledge(Document): self.maximum_loan_value = maximum_loan_value def update_loan(loan, maximum_value_against_pledge): - maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value']) + maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1 + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) From cd2c90451e38716dce9db3e802c60c971a001381 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 22 Oct 2020 21:50:06 +0530 Subject: [PATCH 058/229] fix: Unaccrued interest after disbursal --- erpnext/loan_management/doctype/loan/loan.json | 5 ++--- .../loan_interest_accrual/loan_interest_accrual.py | 8 ++++---- .../doctype/loan_repayment/loan_repayment.py | 7 ++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 8a3b2fa029..b613d22827 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -295,8 +295,7 @@ "default": "0", "fieldname": "is_secured_loan", "fieldtype": "Check", - "label": "Is Secured Loan", - "read_only": 1 + "label": "Is Secured Loan" }, { "default": "0", @@ -351,7 +350,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-21 09:12:26.809228", + "modified": "2020-10-22 11:03:43.697394", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index e31b844953..1fc41f9ea5 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -210,21 +210,21 @@ def make_loan_interest_accrual_entry(args): def get_no_of_days_for_interest_accural(loan, posting_date): - last_interest_accrual_date = get_last_accural_date_in_current_month(loan) + last_interest_accrual_date = get_last_accural_date(loan.name) no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 return no_of_days -def get_last_accural_date_in_current_month(loan): +def get_last_accural_date(loan): last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` - WHERE loan = %s""", (loan.name)) + WHERE loan = %s""", (loan)) if last_posting_date[0][0]: # interest for last interest accrual date is already booked, so add 1 day return add_days(last_posting_date[0][0], 1) else: - return loan.disbursement_date + return frappe.db.get_value('Loan', loan, 'disbursement_date') def days_in_year(year): days = 365 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 6b3fba41c8..12d81d3a24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -14,7 +14,7 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans -from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accural_date class LoanRepayment(AccountsController): @@ -76,14 +76,15 @@ class LoanRepayment(AccountsController): if self.total_interest_paid > self.interest_payable: if not self.is_term_loan: # get last loan interest accrual date - last_accrual_date = frappe.get_value('Loan Interest Accrual', {'loan': self.against_loan}, 'MAX(posting_date)') + last_accrual_date = get_last_accural_date(self.against_loan) # get posting date upto which interest has to be accrued per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, self.rate_of_interest, self.posting_date), 2) no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, - precision)/per_day_interest, 0) + precision)/per_day_interest, 0) - 1 + posting_date = add_days(last_accrual_date, no_of_days) From 2b3f8e0c3b5bab732ec452675a5b21fc72f5404a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 23 Oct 2020 19:02:24 +0530 Subject: [PATCH 059/229] fix: Cancel repayment accrual interest entry on payment cancellation --- .../doctype/loan_repayment/loan_repayment.py | 15 +++++++++++++-- .../loan_repayment_detail.json | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 12d81d3a24..b973cd69e6 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -24,8 +24,10 @@ class LoanRepayment(AccountsController): self.validate_amount() self.allocate_amounts(amounts) - def on_submit(self): + def before_submit(self): self.book_unaccrued_interest() + + def on_submit(self): self.update_paid_amount() self.make_gl_entries() @@ -99,7 +101,8 @@ class LoanRepayment(AccountsController): self.append('repayment_details', { 'loan_interest_accrual': lia.name, 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), - 'paid_principal_amount': 0.0 + 'paid_principal_amount': 0.0, + 'accrual_type': 'Repayment' }) def update_paid_amount(self): @@ -123,6 +126,8 @@ class LoanRepayment(AccountsController): def mark_as_unpaid(self): loan = frappe.get_doc("Loan", self.against_loan) + no_of_repayments = len(self.repayment_details) + for payment in self.repayment_details: frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` - %s, @@ -130,6 +135,12 @@ class LoanRepayment(AccountsController): WHERE name = %s""", (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual)) + # Cancel repayment interest accrual + # checking idx as a preventive measure, repayment accrual will always be the last entry + if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments: + lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual) + lia_doc.cancel() + frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s WHERE name = %s """, (loan.total_amount_paid - self.amount_paid, loan.total_principal_paid - self.principal_amount_paid, self.against_loan)) diff --git a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json index cff1dbb1d2..4b9b191e26 100644 --- a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json +++ b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json @@ -7,7 +7,8 @@ "field_order": [ "loan_interest_accrual", "paid_principal_amount", - "paid_interest_amount" + "paid_interest_amount", + "accrual_type" ], "fields": [ { @@ -27,11 +28,20 @@ "fieldtype": "Currency", "label": "Paid Interest Amount", "options": "Company:company:default_currency" + }, + { + "fetch_from": "loan_interest_accrual.accrual_type", + "fetch_if_empty": 1, + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nDisbursement" } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-04-15 21:50:03.837019", + "modified": "2020-10-23 08:09:18.267030", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Detail", From 0ac40c75aabbbe2ee31dfd0b4f4881e9846dcf6d Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Sun, 25 Oct 2020 01:16:06 +0530 Subject: [PATCH 060/229] chore(UAE VAT): update tesr dependencies --- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 68c163d5bc..05bb701dc2 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -21,7 +21,7 @@ from erpnext.regional.report.uae_vat_201.uae_vat_201 import ( get_reverse_charge_recoverable_tax ) - +test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] class TestUaeVat201(TestCase): def setUp(self): From 0ceae0bd66e7bbd1f8af8f4b20bc50450fbc2202 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Sun, 25 Oct 2020 13:06:42 +0530 Subject: [PATCH 061/229] feat(UAE VAT 201): emirate customizable --- .../report/uae_vat_201/test_uae_vat_201.py | 79 +++++-------------- .../regional/united_arab_emirates/setup.py | 10 +-- 2 files changed, 23 insertions(+), 66 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 05bb701dc2..ceaa3b3dcb 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -34,13 +34,12 @@ class TestUaeVat201(TestCase): make_company("_Test Company UAE VAT", "_TCUV") set_vat_accounts() - make_customers() + make_customer() make_supplier() create_warehouse("_Test UAE VAT Supplier Warehouse", company="_Test Company UAE VAT") - make_item("_Test UAE VAT Item", properties = {"is_zero_rated": 0, "is_exempt": 0}) make_item("_Test UAE VAT Zero Rated Item", properties = {"is_zero_rated": 1, "is_exempt": 0}) make_item("_Test UAE VAT Exempt Item", properties = {"is_zero_rated": 0, "is_exempt": 1}) @@ -122,64 +121,16 @@ def set_vat_accounts(): "doctype": "UAE VAT Settings", }).insert() -def make_customers(): - if not frappe.db.exists("Customer", "_Test Dubai Customer"): +def make_customer(): + if not frappe.db.exists("Customer", "_Test UAE Customer"): customer = frappe.get_doc({ "doctype": "Customer", - "customer_name": "_Test Dubai Customer", + "customer_name": "_Test UAE Customer", "customer_type": "Company", }) customer.insert() else: - customer = frappe.get_doc("Customer", "_Test Dubai Customer") - - if not frappe.db.exists("Customer", "_Test Sharjah Customer"): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test Sharjah Customer", - "customer_type": "Company", - }) - customer.insert() - else: - customer = frappe.get_doc("Customer", "_Test Sharjah Customer") - - if not frappe.db.exists('Address', '_Test Dubai Address'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Dubai Address", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "United Arab Emirates", - "doctype": "Address", - "emirate": "Dubai" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Dubai Customer" - }) - - address.save() - - if not frappe.db.exists('Address', '_Test Sharjah Address'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Sharjah Address", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "United Arab Emirates", - "doctype": "Address", - "emirate": "Sharjah" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Sharjah Customer" - }) - - address.save() + customer = frappe.get_doc("Customer", "_Test UAE Customer") def make_supplier(): @@ -230,7 +181,7 @@ def make_item(item_code, properties=None): def make_sales_invoices(): si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test Dubai Customer', + customer = '_Test UAE Customer', currency = 'AED', warehouse = 'Finished Goods - _TCUV', debit_to = 'Debtors - _TCUV', @@ -248,10 +199,11 @@ def make_sales_invoices(): "description": "VAT 5% @ 5.0", "rate": 5.0 }) + si.emirate = 'Dubai' si.submit() si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test Sharjah Customer', + customer = '_Test UAE Customer', currency = 'AED', warehouse = 'Finished Goods - _TCUV', debit_to = 'Debtors - _TCUV', @@ -262,6 +214,7 @@ def make_sales_invoices(): item = "_Test UAE VAT Item", do_not_save=1 ) + si.emirate = 'Sharjah' si.append("taxes", { "charge_type": "On Net Total", "account_head": "VAT 5% - _TCUV", @@ -272,7 +225,7 @@ def make_sales_invoices(): si.submit() si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test Dubai Customer', + customer = '_Test UAE Customer', currency = 'AED', warehouse = 'Finished Goods - _TCUV', debit_to = 'Debtors - _TCUV', @@ -286,6 +239,8 @@ def make_sales_invoices(): si.tourist_tax_return = 2 + si.emirate = 'Dubai' + si.append("taxes", { "charge_type": "On Net Total", "account_head": "VAT 5% - _TCUV", @@ -296,7 +251,7 @@ def make_sales_invoices(): si.submit() si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test Sharjah Customer', + customer = '_Test UAE Customer', currency = 'AED', warehouse = 'Finished Goods - _TCUV', debit_to = 'Debtors - _TCUV', @@ -305,10 +260,13 @@ def make_sales_invoices(): cost_center = 'Main - _TCUV', sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", item = "_Test UAE VAT Zero Rated Item", + do_not_save=1 ) + si.emirate = 'Sharjah' + si.submit() si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test Sharjah Customer', + customer = '_Test UAE Customer', currency = 'AED', warehouse = 'Finished Goods - _TCUV', debit_to = 'Debtors - _TCUV', @@ -317,7 +275,10 @@ def make_sales_invoices(): cost_center = 'Main - _TCUV', sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", item = "_Test UAE VAT Exempt Item", + do_not_save=1 ) + si.emirate = 'Sharjah' + si.submit() def create_purchase_invoices(): diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index fac6e270db..7f890d6867 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -56,10 +56,10 @@ def make_custom_fields(): dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='emirate', label='Emirate', insert_after='customer_address', - fieldtype='Read Only', fetch_from='customer_address.emirate'), + dict(fieldname='emirate', label='Emirate', insert_after='permit_no', fieldtype='Select', + options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', - insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), + insert_after='emirate', fieldtype='Currency', print_hide=1, default='0'), ] invoice_item_fields = [ @@ -101,10 +101,6 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Data', insert_after='supplier_name'), ], - 'Address': [ - dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', - options='Abu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') - ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields, 'Purchase Receipt': purchase_invoice_fields + invoice_fields, From 5953acab8bb4a9efbbca7649db62d3fce38779f0 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Sun, 25 Oct 2020 15:43:21 +0530 Subject: [PATCH 062/229] chore(UAE VAT 201): cleanup --- .../report/uae_vat_201/test_uae_vat_201.py | 40 +------------------ .../regional/united_arab_emirates/setup.py | 3 +- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index ceaa3b3dcb..3b5d388e25 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -11,14 +11,10 @@ from erpnext.regional.report.uae_vat_201.uae_vat_201 import ( get_total_emiratewise, get_tourist_tax_return_total, get_tourist_tax_return_tax, - get_reverse_charge_total, - get_reverse_charge_tax, get_zero_rated_total, get_exempt_total, get_standard_rated_expenses_total, get_standard_rated_expenses_tax, - get_reverse_charge_recoverable_total, - get_reverse_charge_recoverable_tax ) test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] @@ -62,17 +58,12 @@ class TestUaeVat201(TestCase): self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) - self.assertEqual(get_tourist_tax_return_total(filters),100) self.assertEqual(get_tourist_tax_return_tax(filters),2) - self.assertEqual(get_reverse_charge_total(filters),250) - self.assertEqual(get_reverse_charge_tax(filters),12.5) self.assertEqual(get_zero_rated_total(filters),100) self.assertEqual(get_exempt_total(filters),100) self.assertEqual(get_standard_rated_expenses_total(filters),250) self.assertEqual(get_standard_rated_expenses_tax(filters),1) - self.assertEqual(get_reverse_charge_recoverable_total(filters),250) - self.assertEqual(get_reverse_charge_recoverable_tax(filters),12.5) def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): @@ -304,33 +295,4 @@ def create_purchase_invoices(): pi.recoverable_standard_rated_expenses = 1 - pi.submit() - - pi = make_purchase_invoice( - company="_Test Company UAE VAT", - supplier = '_Test UAE Supplier', - supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - currency = 'AED', - cost_center = 'Main - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - item = "_Test UAE VAT Item", - do_not_save=1, - uom = "Nos" - ) - - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) - - pi.reverse_charge = "Y" - - pi.recoverable_reverse_charge = 100 - - pi.submit() - - + pi.submit() \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 7f890d6867..c1188d6ccd 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -39,8 +39,7 @@ def make_custom_fields(): fieldtype='Read Only', insert_after='supplier_name', fetch_from='supplier.supplier_name_in_arabic', print_hide=1), dict(fieldname='recoverable_standard_rated_expenses', label='Recoverable Standard Rated Expenses (AED)', - insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0', - depends_on="eval:doc.reverse_charge=='N'",), + insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), dict(fieldname='reverse_charge', label='Reverse Charge Applicable', fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1, options='Y\nN', default='N'), From d63fbd79f4f434e3a6d925832d114c9f2b930442 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 14:20:55 +0530 Subject: [PATCH 063/229] fix: Unaccrued interest from last accrual date instead of disbursement date --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index b973cd69e6..c1e83d9305 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -310,6 +310,7 @@ def get_accrued_interest_entries(against_loan): payable_principal_amount - paid_principal_amount > 0) AND docstatus = 1 + ORDER BY posting_date """, (against_loan), as_dict=1) return unpaid_accrued_entries @@ -366,7 +367,8 @@ def get_amounts(amounts, against_loan, posting_date): if due_date: pending_days = date_diff(posting_date, due_date) + 1 else: - pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 + last_accrual_date = get_last_accural_date(against_loan_doc.name) + pending_days = date_diff(posting_date, last_accrual_date) + 1 if pending_days > 0: principal_amount = flt(pending_principal_amount, precision) From 5b4742fd25043532cd9338ace81998c233c9a91d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 16:56:05 +0530 Subject: [PATCH 064/229] fix: Permission fixes for some doctypes --- .../doctype/loan_type/loan_type.json | 4 +++- .../doctype/loan_write_off/loan_write_off.json | 18 +++++++++++++++++- .../process_loan_interest_accrual.json | 6 +++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 5d9232d711..18a97315f0 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -154,13 +154,14 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-17 11:41:17.907683", + "modified": "2020-10-26 07:13:55.029811", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -170,6 +171,7 @@ "report": 1, "role": "Loan Manager", "share": 1, + "submit": 1, "write": 1 }, { diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json index 64623c4b3a..4617a62f5b 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json @@ -116,13 +116,14 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-17 08:30:54.859362", + "modified": "2020-10-26 07:13:43.663924", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Write Off", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -132,6 +133,21 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index bb781b1d56..c1296f759f 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -59,13 +59,14 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-11 11:19:00.531046", + "modified": "2020-10-26 07:14:31.491249", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -75,9 +76,11 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -87,6 +90,7 @@ "report": 1, "role": "Loan Manager", "share": 1, + "submit": 1, "write": 1 } ], From 3f971b23716205351ee45960ee448462ca392a6e Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 16:59:57 +0530 Subject: [PATCH 065/229] fix: copying po no when mapping doc --- .../doctype/sales_invoice/sales_invoice.py | 1 + erpnext/controllers/selling_controller.py | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4b598877d9..af6c6968dc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1401,6 +1401,7 @@ def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): target.ignore_pricing_rule = 1 target.run_method("set_missing_values") + target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 58861715c2..0fbd914fdf 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -371,13 +371,39 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"): - ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order" - sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) - if sales_orders: - po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if po_nos and po_nos[0].get('po_no'): - self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) + self.po_no = '' + if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + self.set_pos_for_sales_invoice() + if self.doctype == 'Delivery Note' and hasattr(self, "items"): + self.set_pos_for_delivery_note() + + def set_pos_for_sales_invoice(self): + ref_fieldname1 = "sales_order" + ref_fieldname2 = "delivery_note" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if delivery_notes: + dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) + if dn_po_nos and dn_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + + def set_pos_for_delivery_note(self): + ref_fieldname1 = "against_sales_order" + ref_fieldname2 = "against_sales_invoice" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + if sales_invoices: + si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) + if si_po_nos and si_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: From a630de56e9d38aa5ed253fb349aa70957e8d8cd4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 18:53:56 +0530 Subject: [PATCH 066/229] fix: Loan disbursement amount validation check --- .../loan_disbursement/loan_disbursement.py | 107 ++++++++++-------- .../doctype/loan_security/loan_security.json | 4 +- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index bfdc8b403e..f58b989a20 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -17,6 +17,7 @@ class LoanDisbursement(AccountsController): def validate(self): self.set_missing_values() + self.validate_disbursal_amount() def on_submit(self): self.set_status_and_amounts() @@ -40,57 +41,21 @@ class LoanDisbursement(AccountsController): if not self.bank_account and self.applicant_type == "Customer": self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") - def set_status_and_amounts(self, cancel=0): + def validate_disbursal_amount(self): + possible_disbursal_amount = get_disbursal_amount(self.against_loan) + if self.disbursed_amount > possible_disbursal_amount: + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) + + def set_status_and_amounts(self, cancel=0): loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0] if cancel: - disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount - total_payment = loan_details.total_payment - - if loan_details.disbursed_amount > loan_details.loan_amount: - topup_amount = loan_details.disbursed_amount - loan_details.loan_amount - if topup_amount > self.disbursed_amount: - topup_amount = self.disbursed_amount - - total_payment = total_payment - topup_amount - - if disbursed_amount == 0: - status = "Sanctioned" - elif disbursed_amount >= loan_details.loan_amount: - status = "Disbursed" - else: - status = "Partially Disbursed" + disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details) else: - disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount - total_payment = loan_details.total_payment - - possible_disbursal_amount = get_disbursal_amount(self.against_loan) - - if self.disbursed_amount > possible_disbursal_amount: - frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) - - if loan_details.status == "Disbursed" and not loan_details.is_term_loan: - process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), - loan=self.against_loan, accrual_type="Disbursement") - - if disbursed_amount > loan_details.loan_amount: - topup_amount = disbursed_amount - loan_details.loan_amount - - if topup_amount < 0: - topup_amount = 0 - - if topup_amount > self.disbursed_amount: - topup_amount = self.disbursed_amount - - total_payment = total_payment + topup_amount - - if flt(disbursed_amount) >= loan_details.loan_amount: - status = "Disbursed" - else: - status = "Partially Disbursed" + disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details) frappe.db.set_value("Loan", self.against_loan, { "disbursement_date": self.disbursement_date, @@ -99,6 +64,54 @@ class LoanDisbursement(AccountsController): "total_payment": total_payment }) + def get_values_on_cancel(self, loan_details): + disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount + total_payment = loan_details.total_payment + + if loan_details.disbursed_amount > loan_details.loan_amount: + topup_amount = loan_details.disbursed_amount - loan_details.loan_amount + if topup_amount > self.disbursed_amount: + topup_amount = self.disbursed_amount + + total_payment = total_payment - topup_amount + + if disbursed_amount == 0: + status = "Sanctioned" + total_payment = loan_details.loan_amount + elif disbursed_amount >= loan_details.loan_amount: + status = "Disbursed" + else: + status = "Partially Disbursed" + + return disbursed_amount, status, total_payment + + def get_values_on_submit(self, loan_details): + disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount + total_payment = loan_details.total_payment + + if loan_details.status == "Disbursed" and not loan_details.is_term_loan: + process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), + loan=self.against_loan, accrual_type="Disbursement") + + if disbursed_amount > loan_details.loan_amount: + topup_amount = disbursed_amount - loan_details.loan_amount + + if topup_amount < 0: + topup_amount = 0 + + if topup_amount > self.disbursed_amount: + topup_amount = self.disbursed_amount + + total_payment = total_payment + topup_amount + + if flt(disbursed_amount) >= loan_details.loan_amount: + status = "Disbursed" + else: + status = "Partially Disbursed" + total_payment = disbursed_amount + + return disbursed_amount, status, total_payment + def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] loan_details = frappe.get_doc("Loan", self.against_loan) @@ -155,7 +168,8 @@ def get_total_pledged_security_value(loan): pledged_securities = get_pledged_security_qty(loan) for security, qty in pledged_securities.items(): - security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 + after_haircut_percentage = 100 - hair_cut_map.get(security) + security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100 return security_value @@ -173,7 +187,8 @@ def get_disbursal_amount(loan): pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - flt(loan_details.total_principal_paid) else: - pending_principal_amount = flt(loan_details.disbursed_amount) + pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) security_value = 0.0 if loan_details.is_secured_loan: diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json index 1d0bb30910..c698601ea4 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.json +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -25,6 +25,7 @@ }, { "fetch_from": "loan_security_type.haircut", + "fetch_if_empty": 1, "fieldname": "haircut", "fieldtype": "Percent", "label": "Haircut %" @@ -64,8 +65,9 @@ "reqd": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-04-29 13:21:26.043492", + "modified": "2020-10-26 07:34:48.601766", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", From 13f4b3fc17f785176a006aea8592dcee588da5dd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 19:01:36 +0530 Subject: [PATCH 067/229] fix: Translation syntax --- .../loan_security_unpledge/loan_security_unpledge.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index d0d25e8897..5d4447bf2b 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -51,9 +51,11 @@ class LoanSecurityUnpledge(Document): for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: - frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. - You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, - frappe.bold(security.loan_security), frappe.bold(self.loan))) + msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom, + frappe.bold(security.loan_security), frappe.bold(self.loan)) + msg += "
" + msg += _("You are trying to unpledge more.") + frappe.throw(msg, title=_("Loan Security Unpledge Error")) qty_after_unpledge = pledged_qty - security.qty ltv_ratio = ltv_ratio_map.get(security.loan_security_type) From 0a6e1b35030e3fc3574d568531c02fbd6f36dd1c Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 21:23:31 +0530 Subject: [PATCH 068/229] fix: refactor --- erpnext/controllers/selling_controller.py | 40 +++++++++-------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 0fbd914fdf..e5405b2e43 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -371,39 +371,29 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - self.po_no = '' if self.doctype == 'Sales Invoice' and hasattr(self, "items"): self.set_pos_for_sales_invoice() if self.doctype == 'Delivery Note' and hasattr(self, "items"): self.set_pos_for_delivery_note() def set_pos_for_sales_invoice(self): - ref_fieldname1 = "sales_order" - ref_fieldname2 = "delivery_note" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if delivery_notes: - dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) - if dn_po_nos and dn_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'sales_order', po_nos) + self.get_po_nos('Delivery Note', 'delivery_note', po_nos) + self.po_no = ', '.join(list(set(po_nos))) def set_pos_for_delivery_note(self): - ref_fieldname1 = "against_sales_order" - ref_fieldname2 = "against_sales_invoice" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - if sales_invoices: - si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) - if si_po_nos and si_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'against_sales_order', po_nos) + self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) + self.po_no = ', '.join(list(set(po_nos))) + + def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): + doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) + if doc_list: + po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) + if po_no_list and po_no_list[0].get('po_no'): + po_nos += [d.po_no for d in po_no_list if d.po_no] def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: From fa085d7f4bcb163ce02b44db3bc07cd26f19280a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 27 Oct 2020 13:18:30 +0530 Subject: [PATCH 069/229] fix: Payment Terms not fetched in Purchase Invoice from Purchase Receipt --- erpnext/accounts/party.py | 6 +++--- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e8d3cd322b..64268b8064 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -59,7 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) if not party_details.get("currency"): party_details["currency"] = currency @@ -315,7 +315,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None): due_date = None if (bill_date or posting_date) and party: due_date = bill_date or posting_date - template_name = get_pyt_term_template(party, party_type, company) + template_name = get_payment_terms_template(party, party_type, company) if template_name: due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") @@ -422,7 +422,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup @frappe.whitelist() -def get_pyt_term_template(party_name, party_type, company=None): +def get_payment_terms_template(party_name, party_type, company=None): if party_type not in ("Customer", "Supplier"): return template = None diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index faa9dd944b..d964669830 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -533,6 +533,8 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + from erpnext.accounts.party import get_payment_terms_template + doc = frappe.get_doc('Purchase Receipt', source_name) returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) @@ -543,6 +545,7 @@ def make_purchase_invoice(source_name, target_doc=None): doc = frappe.get_doc(target) doc.ignore_pricing_rule = 1 + doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") From 67f2c82d907d2aae96287352b9e5112e794eb26e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 27 Oct 2020 14:49:43 +0530 Subject: [PATCH 070/229] chore: Test case for Payment Terms in PI from PR --- .../purchase_receipt/test_purchase_receipt.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a1e01bf7ce..253edb02c3 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -42,6 +42,30 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1) def test_make_purchase_invoice(self): + if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): + frappe.get_doc({ + 'doctype': 'Payment Terms Template', + 'template_name': '_Test Payment Terms Template For Purchase Invoice', + 'allocate_payment_based_on_payment_terms': 1, + 'terms': [ + { + 'doctype': 'Payment Terms Template Detail', + 'invoice_portion': 50.00, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 00 + }, + { + 'doctype': 'Payment Terms Template Detail', + 'invoice_portion': 50.00, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 30 + }] + }).insert() + + template = frappe.db.get_value('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice') + old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms") + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template) + pr = make_purchase_receipt(do_not_save=True) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) pr.submit() @@ -51,10 +75,23 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pi.doctype, "Purchase Invoice") self.assertEqual(len(pi.get("items")), len(pr.get("items"))) - # modify rate + # test maintaining same rate throughout purchade cycle pi.get("items")[0].rate = 200 self.assertRaises(frappe.ValidationError, frappe.get_doc(pi).submit) + # test if payment terms are fetched and set in PI + self.assertEqual(pi.payment_terms_template, template) + self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[0].invoice_portion, 50) + self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[1].invoice_portion, 50) + + # teardown + pi.delete() # draft PI + pr.cancel() + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) + frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() + def test_purchase_receipt_no_gl_entry(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') From 81f2efd9b04b307af66b45cf69ca8a06021617f5 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 28 Oct 2020 10:28:25 +0530 Subject: [PATCH 071/229] chore(UAE VAT 201): Rename emirate to vat emirate --- .../regional/report/uae_vat_201/test_uae_vat_201.py | 10 +++++----- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 4 ++-- erpnext/regional/united_arab_emirates/setup.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 3b5d388e25..3037a07698 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -190,7 +190,7 @@ def make_sales_invoices(): "description": "VAT 5% @ 5.0", "rate": 5.0 }) - si.emirate = 'Dubai' + si.vat_emirate = 'Dubai' si.submit() si = create_sales_invoice(company="_Test Company UAE VAT", @@ -205,7 +205,7 @@ def make_sales_invoices(): item = "_Test UAE VAT Item", do_not_save=1 ) - si.emirate = 'Sharjah' + si.vat_emirate = 'Sharjah' si.append("taxes", { "charge_type": "On Net Total", "account_head": "VAT 5% - _TCUV", @@ -230,7 +230,7 @@ def make_sales_invoices(): si.tourist_tax_return = 2 - si.emirate = 'Dubai' + si.vat_emirate = 'Dubai' si.append("taxes", { "charge_type": "On Net Total", @@ -253,7 +253,7 @@ def make_sales_invoices(): item = "_Test UAE VAT Zero Rated Item", do_not_save=1 ) - si.emirate = 'Sharjah' + si.vat_emirate = 'Sharjah' si.submit() si = create_sales_invoice(company="_Test Company UAE VAT", @@ -268,7 +268,7 @@ def make_sales_invoices(): item = "_Test UAE VAT Exempt Item", do_not_save=1 ) - si.emirate = 'Sharjah' + si.vat_emirate = 'Sharjah' si.submit() def create_purchase_invoices(): diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 5bed0e2463..ddbdf96c6a 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -147,9 +147,9 @@ def append_data(data, no, legend, amount, vat_amount): def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" return frappe.db.sql(f""" - select emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` + select vat_emirate as emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` where docstatus = 1 {get_conditions(filters)} - group by `tabSales Invoice`.emirate; + group by `tabSales Invoice`.vat_emirate; """, filters) def get_emirates(): diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index c1188d6ccd..62156e46be 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -55,10 +55,10 @@ def make_custom_fields(): dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='emirate', label='Emirate', insert_after='permit_no', fieldtype='Select', + dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', - insert_after='emirate', fieldtype='Currency', print_hide=1, default='0'), + insert_after='vat_emirate', fieldtype='Currency', print_hide=1, default='0'), ] invoice_item_fields = [ From c9a6135d6cddc9480db89b26ccf38628571d937e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 28 Oct 2020 13:38:15 +0530 Subject: [PATCH 072/229] fix: Interest accrual after loan topup --- erpnext/loan_management/doctype/loan/loan.json | 5 +++-- .../doctype/loan_disbursement/loan_disbursement.py | 8 +++++--- .../doctype/loan_disbursement/test_loan_disbursement.py | 6 ++++++ .../loan_interest_accrual/loan_interest_accrual.py | 2 +- .../doctype/loan_repayment/loan_repayment.py | 7 ++++--- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index b613d22827..23996afd21 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -344,13 +344,14 @@ "fieldtype": "Currency", "label": "Written Off Amount", "no_copy": 1, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-22 11:03:43.697394", + "modified": "2020-10-27 23:37:02.785940", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index f58b989a20..949e1412e2 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -77,7 +77,7 @@ class LoanDisbursement(AccountsController): if disbursed_amount == 0: status = "Sanctioned" - total_payment = loan_details.loan_amount + elif disbursed_amount >= loan_details.loan_amount: status = "Disbursed" else: @@ -89,7 +89,7 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if loan_details.status == "Disbursed" and not loan_details.is_term_loan: + if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), loan=self.against_loan, accrual_type="Disbursement") @@ -108,7 +108,6 @@ class LoanDisbursement(AccountsController): status = "Disbursed" else: status = "Partially Disbursed" - total_payment = disbursed_amount return disbursed_amount, status, total_payment @@ -199,6 +198,9 @@ def get_disbursal_amount(loan): disbursal_amount = flt(security_value) - flt(pending_principal_amount) + if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount: + disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount + return disbursal_amount diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index aaaeea8c4e..a8753877a6 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -69,6 +69,12 @@ class TestLoanDisbursement(unittest.TestCase): # After repayment loan disbursement entry should go through make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) + # check for disbursement accrual + loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name, + 'accrual_type': 'Disbursement'}) + + self.assertTrue(loan_interest_accrual) + def test_loan_topup_with_additional_pledge(self): pledge = [{ "loan_security": "Test Security 1", diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 1fc41f9ea5..d0b957de56 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -218,7 +218,7 @@ def get_no_of_days_for_interest_accural(loan, posting_date): def get_last_accural_date(loan): last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` - WHERE loan = %s""", (loan)) + WHERE loan = %s and docstatus = 1""", (loan)) if last_posting_date[0][0]: # interest for last interest accrual date is already booked, so add 1 day diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c1e83d9305..63d388daf5 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -301,7 +301,8 @@ def get_accrued_interest_entries(against_loan): unpaid_accrued_entries = frappe.db.sql( """ SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount, - payable_principal_amount - paid_principal_amount as payable_principal_amount + payable_principal_amount - paid_principal_amount as payable_principal_amount, + accrual_type FROM `tabLoan Interest Accrual` WHERE @@ -342,7 +343,7 @@ def get_amounts(amounts, against_loan, posting_date): no_of_late_days = date_diff(posting_date, add_days(due_date, loan_type_details.grace_period_in_days)) - if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): + if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 total_pending_interest += entry.interest_amount @@ -353,7 +354,7 @@ def get_amounts(amounts, against_loan, posting_date): 'payable_principal_amount': flt(entry.payable_principal_amount, precision) }) - if not final_due_date: + if due_date and not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): From 2c00549dfe9d87ed1b21a600df7275d0459c86c8 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 28 Oct 2020 22:54:56 +0530 Subject: [PATCH 073/229] refactor(UAE VAT 201): Use frappe api instead sql --- .../report/uae_vat_201/uae_vat_201.py | 152 ++++++++++-------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index ddbdf96c6a..744eb87d55 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -146,11 +146,14 @@ def append_data(data, no, legend, amount, vat_amount): def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" - return frappe.db.sql(f""" - select vat_emirate as emirate, sum(total), sum(total_taxes_and_charges) from `tabSales Invoice` - where docstatus = 1 {get_conditions(filters)} - group by `tabSales Invoice`.vat_emirate; - """, filters) + query_filters = get_filters(filters) + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Sales Invoice', + filters = query_filters, + fields = ['vat_emirate as emirate','sum(total)', 'sum(total_taxes_and_charges)'], + group_by='vat_emirate', + as_list=True + ) def get_emirates(): """Returns a List of emirates in the order that they are to be displayed.""" @@ -174,28 +177,40 @@ def get_conditions(filters): conditions += opts[1] return conditions +def get_filters(filters): + """The conditions to be used to filter data to calculate the total sale.""" + query_filters = {} + if filters.get("company"): + query_filters["company"] = ['=', filters['company']] + if filters.get("from_date"): + query_filters["posting_date"] = ['>=', filters['from_date']] + if filters.get("from_date"): + query_filters["posting_date"] = ['<=', filters['to_date']] + return query_filters + def get_reverse_charge_total(filters): """Returns the sum of the total of each Purchase invoice made.""" - conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(total) from - `tabPurchase Invoice` - where - reverse_charge = "Y" - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + query_filters = get_filters(filters) + query_filters['reverse_charge'] = ['=', 'Y'] + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Purchase Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" select sum(debit) from - `tabPurchase Invoice` inner join `tabGL Entry` - on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name + `tabPurchase Invoice` p inner join `tabGL Entry` gl + on gl.voucher_no = p.name where - `tabPurchase Invoice`.reverse_charge = "Y" - and `tabPurchase Invoice`.docstatus = 1 - and `tabGL Entry`.docstatus = 1 + p.reverse_charge = "Y" + and p.docstatus = 1 + and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 @@ -212,75 +227,80 @@ def get_conditions_join(filters): def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" - conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(total) from - `tabPurchase Invoice` - where - reverse_charge = "Y" - and recoverable_reverse_charge > 0 - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + query_filters = get_filters(filters) + query_filters['reverse_charge'] = ['=', 'Y'] + query_filters['recoverable_reverse_charge'] = ['>', '0'] + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Purchase Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" - select sum(debit * `tabPurchase Invoice`.recoverable_reverse_charge / 100) from - `tabPurchase Invoice` inner join `tabGL Entry` - on `tabGL Entry`.voucher_no = `tabPurchase Invoice`.name + select sum(debit * p.recoverable_reverse_charge / 100) from + `tabPurchase Invoice` p inner join `tabGL Entry` gl + on gl.voucher_no = p.name where - `tabPurchase Invoice`.reverse_charge = "Y" - and `tabPurchase Invoice`.docstatus = 1 - and `tabPurchase Invoice`.recoverable_reverse_charge > 0 - and `tabGL Entry`.docstatus = 1 + p.reverse_charge = "Y" + and p.docstatus = 1 + and p.recoverable_reverse_charge > 0 + and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" - conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(total) from - `tabPurchase Invoice` - where - recoverable_standard_rated_expenses > 0 - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + query_filters = get_filters(filters) + query_filters['recoverable_standard_rated_expenses'] = ['>', 0] + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Purchase Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" - conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(recoverable_standard_rated_expenses) from - `tabPurchase Invoice` - where - recoverable_standard_rated_expenses > 0 - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + query_filters = get_filters(filters) + query_filters['recoverable_standard_rated_expenses'] = ['>', 0] + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Purchase Invoice', + filters = query_filters, + fields = ['sum(recoverable_standard_rated_expenses)'], + as_list=True, + limit = 1 + )[0][0] or 0 def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" - conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(total) from - `tabSales Invoice` - where - tourist_tax_return > 0 - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + query_filters = get_filters(filters) + query_filters['tourist_tax_return'] = ['>', 0] + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Sales Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" - conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(tourist_tax_return) from - `tabSales Invoice` - where - tourist_tax_return > 0 - and docstatus = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + query_filters = get_filters(filters) + query_filters['tourist_tax_return'] = ['>', 0] + query_filters['docstatus'] = ['=', 1] + return frappe.db.get_list('Sales Invoice', + filters = query_filters, + fields = ['sum(tourist_tax_return)'], + as_list=True, + limit = 1 + )[0][0] or 0 def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" From 08846f6b11d88a0cd81888dc7747d3da992cff16 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 28 Oct 2020 23:33:56 +0530 Subject: [PATCH 074/229] refactor(UAE VAT 201): replace cartesian product to inner joins --- .../report/uae_vat_201/uae_vat_201.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 744eb87d55..ba6ff69361 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -9,7 +9,6 @@ def execute(filters=None): columns = get_columns() data, emirates, amounts_by_emirate = get_data(filters) chart = get_chart(emirates, amounts_by_emirate) - return columns, data, None, chart def get_columns(): @@ -50,7 +49,6 @@ def get_data(filters = None): append_vat_on_expenses(data, filters) return data, emirates, amounts_by_emirate - def get_chart(emirates, amounts_by_emirate): """Returns chart data.""" labels = [] @@ -167,16 +165,6 @@ def get_emirates(): 'Fujairah' ] -def get_conditions(filters): - """The conditions to be used to filter data to calculate the total sale.""" - conditions = "" - for opts in (("company", " and company=%(company)s"), - ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] - return conditions - def get_filters(filters): """The conditions to be used to filter data to calculate the total sale.""" query_filters = {} @@ -215,16 +203,6 @@ def get_reverse_charge_tax(filters): {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 -def get_conditions_join(filters): - """The conditions to be used to filter data to calculate the total vat.""" - conditions = "" - for opts in (("company", " and `tabPurchase Invoice`.company=%(company)s"), - ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), - ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] - return conditions - def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) @@ -254,6 +232,16 @@ def get_reverse_charge_recoverable_tax(filters): {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 +def get_conditions_join(filters): + """The conditions to be used to filter data to calculate the total vat.""" + conditions = "" + for opts in (("company", " and `tabPurchase Invoice`.company=%(company)s"), + ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), + ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s")): + if filters.get(opts[0]): + conditions += opts[1] + return conditions + def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) @@ -307,8 +295,9 @@ def get_zero_rated_total(filters): conditions = get_conditions(filters) return frappe.db.sql(""" select sum(i.base_amount) as total from - `tabSales Invoice Item` i, `tabSales Invoice` s - where s.docstatus = 1 and i.parent = s.name and i.is_zero_rated = 1 + `tabSales Invoice Item` i inner join `tabSales Invoice` s + on i.parent = s.name + where s.docstatus = 1 and i.is_zero_rated = 1 {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 @@ -317,7 +306,18 @@ def get_exempt_total(filters): conditions = get_conditions(filters) return frappe.db.sql(""" select sum(i.base_amount) as total from - `tabSales Invoice Item` i, `tabSales Invoice` s - where s.docstatus = 1 and i.parent = s.name and i.is_exempt = 1 + `tabSales Invoice Item` i inner join `tabSales Invoice` s + on i.parent = s.name + where s.docstatus = 1 and i.is_exempt = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 \ No newline at end of file + """.format(where_conditions=conditions), filters)[0][0] or 0 + +def get_conditions(filters): + """The conditions to be used to filter data to calculate the total sale.""" + conditions = "" + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): + if filters.get(opts[0]): + conditions += opts[1] + return conditions \ No newline at end of file From bf261a6c394569ec34874a28eff7be511e67ba88 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Thu, 29 Oct 2020 00:07:52 +0530 Subject: [PATCH 075/229] fix(UAE VAT 201): fix helper --- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index ba6ff69361..758da4205a 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -235,9 +235,9 @@ def get_reverse_charge_recoverable_tax(filters): def get_conditions_join(filters): """The conditions to be used to filter data to calculate the total vat.""" conditions = "" - for opts in (("company", " and `tabPurchase Invoice`.company=%(company)s"), - ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), - ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s")): + for opts in (("company", " and p.company=%(company)s"), + ("from_date", " and p.posting_date>=%(from_date)s"), + ("to_date", " and p.posting_date<=%(to_date)s")): if filters.get(opts[0]): conditions += opts[1] return conditions From eb3084b072654573ff4dd3ad97590f15878e48b0 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Thu, 29 Oct 2020 09:52:17 +0530 Subject: [PATCH 076/229] refactor(UAE VAT 201): break functions --- .../report/uae_vat_201/uae_vat_201.py | 13 +++++++ .../regional/united_arab_emirates/utils.py | 38 ++++++++++--------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 758da4205a..216762bc8d 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -113,6 +113,7 @@ def standard_rated_expenses_emiratewise(data, filters): "amount": frappe.format(amount, 'Currency'), "vat_amount": frappe.format(vat, 'Currency'), } + amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate) for d, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: @@ -125,6 +126,18 @@ def standard_rated_expenses_emiratewise(data, filters): frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) return emirates, amounts_by_emirate +def append_emiratewise_expenses(data, emirates, amounts_by_emirate): + """Append emiratewise standard rated expenses and vat.""" + for d, emirate in enumerate(emirates, 97): + if emirate in amounts_by_emirate: + amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(d)) + amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) + data.append(amounts_by_emirate[emirate]) + else: + append_data(data, _('1{0}').format(chr(d)), + _('Standard rated supplies in {0}').format(emirate), + frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) + return amounts_by_emirate def append_vat_on_expenses(data, filters): """Appends Expenses and All Other Inputs.""" diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 7edc2b6720..7430db4fce 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -123,25 +123,29 @@ def make_regional_gl_entries(gl_entries, doc): for tax in doc.get('taxes'): if tax.category not in ("Total", "Valuation and Total"): continue - - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: - account_currency = get_account_currency(tax.account_head) - - gl_entries.append(doc.get_gl_dict( - { - "account": tax.account_head, - "cost_center": tax.cost_center, - "posting_date": doc.posting_date, - "against": doc.supplier, - dr_or_cr: tax.base_tax_amount_after_discount_amount, - dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ - if account_currency==doc.company_currency \ - else tax.tax_amount_after_discount_amount - }, account_currency, item=tax) - ) + gl_entries = make_gl_entry(tax, gl_entries, doc, tax_accounts) return gl_entries +def make_gl_entry(tax, gl_entries, doc, tax_accounts): + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: + account_currency = get_account_currency(tax.account_head) + + gl_entries.append(doc.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "posting_date": doc.posting_date, + "against": doc.supplier, + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ + if account_currency==doc.company_currency \ + else tax.tax_amount_after_discount_amount + }, account_currency, item=tax) + ) + return gl_entries + + def validate_returns(doc, method): """Standard Rated expenses should not be set when Reverse Charge Applicable is set.""" country = frappe.get_cached_value('Company', doc.company, 'country') From 55c78b17bab76b9feb6aa5bb870858ad9e7df93c Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 29 Oct 2020 12:43:43 +0530 Subject: [PATCH 077/229] fix(UAE VAT 201): change desk page record --- erpnext/accounts/desk_page/accounting/accounting.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index db01254919..64e2b246a7 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -83,7 +83,7 @@ { "hidden": 0, "label": "Value-Added Tax (VAT UAE)", - "links": "[\n {\n \"label\": \"UAE VAT Setting\",\n \"name\": \"UAE VAT Setting\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -158,4 +158,4 @@ "type": "Dashboard" } ] -} \ No newline at end of file +} From 900290413185010aba6775878a2be1121bdcdf85 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 29 Oct 2020 17:12:06 +0530 Subject: [PATCH 078/229] feat: added project link in timesheet form --- .../projects/doctype/timesheet/timesheet.js | 17 +- .../projects/doctype/timesheet/timesheet.json | 1389 ++++------------- 2 files changed, 320 insertions(+), 1086 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 607c3fd974..b068245a8b 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -133,6 +133,11 @@ frappe.ui.form.on("Timesheet", { frm: frm }); }, + + project: function(frm) { + set_project_in_timelog(frm); + }, + }); frappe.ui.form.on("Timesheet Detail", { @@ -162,7 +167,11 @@ frappe.ui.form.on("Timesheet Detail", { frappe.model.set_value(cdt, cdn, "hours", hours); }, - time_logs_add: function(frm) { + time_logs_add: function(frm, cdt, cdn) { + if(frm.doc.project) { + frappe.model.set_value(cdt, cdn, 'project', frm.doc.project); + } + var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row'); $trigger_again.on('click', () => { $('.form-grid') @@ -297,3 +306,9 @@ const set_employee_and_company = function(frm) { } }); }; + +function set_project_in_timelog(frm) { + if(frm.doc.project){ + erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "time_logs", "project"); + } +} \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index c29c11b746..4c2edf4f03 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -1,1133 +1,352 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-02-28 17:57:33", - "custom": 0, - "description": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2013-02-28 17:57:33", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "company", + "sales_invoice", + "column_break_3", + "salary_slip", + "status", + "project", + "employee_detail", + "employee", + "employee_name", + "department", + "column_break_9", + "user", + "start_date", + "end_date", + "section_break_5", + "time_logs", + "working_hours", + "total_hours", + "billing_details", + "total_billable_hours", + "total_billed_hours", + "total_costing_amount", + "column_break_10", + "total_billable_amount", + "total_billed_amount", + "per_billed", + "section_break_18", + "note", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "{employee_name}", - "fieldname": "title", - "fieldtype": "Data", - "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": "Title", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "allow_on_submit": 1, + "default": "{employee_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 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": "Series", - "length": 0, - "no_copy": 0, - "options": "TS-.YYYY.-", - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "TS-.YYYY.-", + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "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": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "", - "fieldname": "sales_invoice", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Invoice", - "length": 0, - "no_copy": 1, - "options": "Sales Invoice", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "sales_invoice", + "fieldtype": "Link", + "label": "Sales Invoice", + "no_copy": 1, + "options": "Sales Invoice", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "salary_slip", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Salary Slip", - "length": 0, - "no_copy": 1, - "options": "Salary Slip", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "salary_slip", + "fieldtype": "Link", + "label": "Salary Slip", + "no_copy": 1, + "options": "Salary Slip", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Draft", - "fieldname": "status", - "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": 1, - "label": "Status", - "length": 0, - "no_copy": 1, - "options": "Draft\nSubmitted\nBilled\nPayslip\nCompleted\nCancelled", - "permlevel": 0, - "print_hide": 1, - "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 - }, + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nBilled\nPayslip\nCompleted\nCancelled", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "depends_on": "eval:!doc.work_order || doc.docstatus == 1", - "fieldname": "employee_detail", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Detail", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!doc.work_order || doc.docstatus == 1", + "fieldname": "employee_detail", + "fieldtype": "Section Break", + "label": "Employee Detail" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "", - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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": "employee", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "employee", - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "depends_on": "employee", + "fieldname": "employee_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Employee Name", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "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 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "user", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "user", + "fieldtype": "Link", + "in_global_search": 1, + "label": "User", + "options": "User", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start_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": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "print_hide": 1, + "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_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "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": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "time_logs", - "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": "Time Sheets", - "length": 0, - "no_copy": 0, - "options": "Timesheet Detail", - "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, - "unique": 0 - }, + "fieldname": "time_logs", + "fieldtype": "Table", + "label": "Time Sheets", + "options": "Timesheet Detail", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "working_hours", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "working_hours", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "description": "", - "fieldname": "total_hours", - "fieldtype": "Float", - "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": "Total Working Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "allow_on_submit": 1, + "default": "0", + "fieldname": "total_hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Total Working Hours", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "billing_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing Details", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "billing_details", + "fieldtype": "Section Break", + "label": "Billing Details", + "permlevel": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_billable_hours", - "fieldtype": "Float", - "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": "Total Billable Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "allow_on_submit": 1, + "fieldname": "total_billable_hours", + "fieldtype": "Float", + "label": "Total Billable Hours", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_billed_hours", - "fieldtype": "Float", - "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": "Total Billed Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "allow_on_submit": 1, + "fieldname": "total_billed_hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Total Billed Hours", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_costing_amount", - "fieldtype": "Currency", - "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": "Total Costing Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "allow_on_submit": 1, + "fieldname": "total_costing_amount", + "fieldtype": "Currency", + "label": "Total Costing Amount", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "", - "description": "", - "fieldname": "total_billable_amount", - "fieldtype": "Currency", - "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": "Total Billable Amount", - "length": 0, - "no_copy": 0, - "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 - }, + "allow_on_submit": 1, + "default": "0", + "fieldname": "total_billable_amount", + "fieldtype": "Currency", + "label": "Total Billable Amount", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_billed_amount", - "fieldtype": "Currency", - "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": "Total Billed Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "allow_on_submit": 1, + "fieldname": "total_billed_amount", + "fieldtype": "Currency", + "label": "Total Billed Amount", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "per_billed", - "fieldtype": "Percent", - "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": "% Amount Billed", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, + "allow_on_submit": 1, + "fieldname": "per_billed", + "fieldtype": "Percent", + "label": "% Amount Billed", + "no_copy": 1, + "print_hide": 1, + "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_18", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_18", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "note", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Note", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "note", + "fieldtype": "Text Editor", + "label": "Note" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Timesheet", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Timesheet", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-clock-o", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-05 21:54:02.654690", - "modified_by": "Administrator", - "module": "Projects", - "name": "Timesheet", - "owner": "Administrator", + ], + "icon": "fa fa-clock-o", + "idx": 1, + "is_submittable": 1, + "links": [], + "modified": "2020-10-29 07:50:35.938231", + "modified_by": "Administrator", + "module": "Projects", + "name": "Timesheet", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Projects User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "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, + "create": 1, + "read": 1, + "role": "Employee", "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "submit": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "permlevel": 1, + "read": 1, + "role": "Accounts User", "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "title" } \ No newline at end of file From 6bf507a54bca2057921c97cf7b09ee5ac2c9ba7c Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 30 Oct 2020 13:59:47 +0530 Subject: [PATCH 079/229] chore: (MR)Added delivery date in SO dialog --- erpnext/stock/doctype/material_request/material_request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 8c470988c8..14f0aa5816 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -173,7 +173,8 @@ frappe.ui.form.on('Material Request', { source_doctype: "Sales Order", target: frm, setters: { - customer: frm.doc.customer || undefined + customer: frm.doc.customer || undefined, + delivery_date: undefined, }, get_query_filters: { docstatus: 1, From e14fdf4a3c281342093e17d2705f0af7f8910bb6 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 30 Oct 2020 17:06:00 +0530 Subject: [PATCH 080/229] chore: RFQ cleanup and Title case for 'Get Items From' - Cleanup up RFQ Get Items From Dialogs - Title cased 'Get items from' to 'Get Items From' --- .../purchase_invoice/purchase_invoice.js | 4 +- .../doctype/sales_invoice/sales_invoice.js | 12 ++--- .../doctype/purchase_order/purchase_order.js | 4 +- .../request_for_quotation.js | 50 +++++++++++-------- .../supplier_quotation/supplier_quotation.js | 4 +- .../maintenance_schedule.js | 2 +- .../maintenance_visit/maintenance_visit.js | 6 +-- erpnext/public/js/controllers/buying.js | 2 +- .../selling/doctype/quotation/quotation.js | 2 +- .../doctype/sales_order/sales_order.js | 2 +- .../doctype/delivery_note/delivery_note.js | 2 +- .../material_request/material_request.js | 4 +- .../purchase_receipt/purchase_receipt.js | 2 +- .../stock/doctype/stock_entry/stock_entry.js | 10 ++-- 14 files changed, 58 insertions(+), 48 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index fe5301d5c8..62213abaa3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -107,7 +107,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ company: me.frm.doc.company } }) - }, __("Get items from")); + }, __("Get Items From")); this.frm.add_custom_button(__('Purchase Receipt'), function() { erpnext.utils.map_current_doc({ @@ -125,7 +125,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ is_return: 0 } }) - }, __("Get items from")); + }, __("Get Items From")); } this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes"); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 9af584e0b1..502e65ed8d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -199,7 +199,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte company: me.frm.doc.company } }) - }, __("Get items from")); + }, __("Get Items From")); }, quotation_btn: function() { @@ -223,7 +223,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte company: me.frm.doc.company } }) - }, __("Get items from")); + }, __("Get Items From")); }, delivery_note_btn: function() { @@ -251,7 +251,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte }; } }); - }, __("Get items from")); + }, __("Get Items From")); }, tc_name: function() { @@ -812,10 +812,10 @@ frappe.ui.form.on('Sales Invoice', { if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) { frm.add_custom_button(__('Healthcare Services'), function() { get_healthcare_services_to_invoice(frm); - },"Get items from"); + },"Get Items From"); frm.add_custom_button(__('Prescriptions'), function() { get_drugs_to_invoice(frm); - },"Get items from"); + },"Get Items From"); } } else { @@ -1080,7 +1080,7 @@ var get_drugs_to_invoice = function(frm) { description:'Quantity will be calculated only for items which has "Nos" as UoM. You may change as required for each invoice item.', get_query: function(doc) { return { - filters: { + filters: { patient: dialog.get_value("patient"), company: frm.doc.company, docstatus: 1 diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 9f2b9714f7..3c6ebd6604 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -366,7 +366,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( per_ordered: ["<", 99.99], } }) - }, __("Get items from")); + }, __("Get Items From")); this.frm.add_custom_button(__('Supplier Quotation'), function() { @@ -382,7 +382,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( status: ["!=", "Stopped"], } }) - }, __("Get items from")); + }, __("Get Items From")); this.frm.add_custom_button(__('Update rate as per last purchase'), function() { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index f56c9b4f0c..49f34a2b92 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -217,13 +217,15 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e source_doctype: "Material Request", target: me.frm, setters: { - company: me.frm.doc.company + schedule_date: undefined, + status: undefined }, get_query_filters: { material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99] + per_ordered: ["<", 99.99], + company: me.frm.doc.company } }) }, __("Get Items From")); @@ -236,32 +238,40 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e source_doctype: "Opportunity", target: me.frm, setters: { - company: me.frm.doc.company + party_name: undefined, + opportunity_from: undefined, + status: undefined }, + get_query_filters: { + status: ["not in", ["Closed", "Lost"]], + company: me.frm.doc.company + } }) }, __("Get Items From")); // Get items from open Material Requests based on supplier this.frm.add_custom_button(__('Possible Supplier'), function() { // Create a dialog window for the user to pick their supplier - var d = new frappe.ui.Dialog({ + var dialog = new frappe.ui.Dialog({ title: __('Select Possible Supplier'), fields: [ - {fieldname: 'supplier', fieldtype:'Link', options:'Supplier', label:'Supplier', reqd:1}, - {fieldname: 'ok_button', fieldtype:'Button', label:'Get Items from Material Requests'}, - ] - }); - - // On the user clicking the ok button - d.fields_dict.ok_button.input.onclick = function() { - var btn = d.fields_dict.ok_button.input; - var v = d.get_values(); - if(v) { - $(btn).set_working(); + { + fieldname: 'supplier', + fieldtype:'Link', + options:'Supplier', + label:'Supplier', + reqd:1, + description: __("Get Items from Material Requests against this Supplier") + } + ], + primary_action_label: __("Get Items"), + primary_action: (args) => { + if(!args) return; + dialog.hide(); erpnext.utils.map_current_doc({ method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_item_from_material_requests_based_on_supplier", - source_name: v.supplier, + source_name: args.supplier, target: me.frm, setters: { company: me.frm.doc.company @@ -273,11 +283,11 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e per_ordered: ["<", 99.99] } }); - $(btn).done_working(); - d.hide(); + dialog.hide(); } - } - d.show(); + }); + + dialog.show(); }, __("Get Items From")); // Get Suppliers diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index 3376e82956..ab96973ced 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -46,7 +46,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext per_ordered: ["<", 99.99] } }) - }, __("Get items from")); + }, __("Get Items From")); this.frm.add_custom_button(__("Request for Quotation"), function() { @@ -67,7 +67,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier" }) - }, __("Get items from")); + }, __("Get Items From")); } }, diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index e940b6050c..ddbcdfde57 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -66,7 +66,7 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({ company: me.frm.doc.company } }); - }, __("Get items from")); + }, __("Get Items From")); } else if (this.frm.doc.docstatus === 1) { this.frm.add_custom_button(__('Create Maintenance Visit'), function() { frappe.model.open_mapped_doc({ diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 2e2a9ce040..4cbb02a5b3 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -62,7 +62,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ company: me.frm.doc.company } }) - }, __("Get items from")); + }, __("Get Items From")); this.frm.add_custom_button(__('Warranty Claim'), function() { erpnext.utils.map_current_doc({ @@ -78,7 +78,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ company: me.frm.doc.company } }) - }, __("Get items from")); + }, __("Get Items From")); this.frm.add_custom_button(__('Sales Order'), function() { erpnext.utils.map_current_doc({ @@ -94,7 +94,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({ order_type: me.frm.doc.order_type, } }) - }, __("Get items from")); + }, __("Get Items From")); } }, }); diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index cb76c87b62..58ac38f0a8 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -276,7 +276,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ var me = this; this.frm.add_custom_button(__("Product Bundle"), function() { erpnext.buying.get_items_from_product_bundle(me.frm); - }, __("Get items from")); + }, __("Get Items From")); }, shipping_address: function(){ diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 12f32602f5..661e107e1e 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -116,7 +116,7 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ company: me.frm.doc.company } }) - }, __("Get items from"), "btn-default"); + }, __("Get Items From"), "btn-default"); } this.toggle_reqd_lead_customer(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 989bd33e42..a146ae3dd3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -236,7 +236,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( status: ["!=", "Lost"] } }) - }, __("Get items from")); + }, __("Get Items From")); } this.order_type(doc); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 19d0bec26c..251a26a592 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -151,7 +151,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( project: me.frm.doc.project || undefined, } }) - }, __("Get items from")); + }, __("Get Items From")); } } diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 14f0aa5816..f004e9145a 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -90,7 +90,7 @@ frappe.ui.form.on('Material Request', { make_custom_buttons: function(frm) { if (frm.doc.docstatus==0) { frm.add_custom_button(__("Bill of Materials"), - () => frm.events.get_items_from_bom(frm), __("Get items from")); + () => frm.events.get_items_from_bom(frm), __("Get Items From")); } if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { @@ -147,7 +147,7 @@ frappe.ui.form.on('Material Request', { if (frm.doc.docstatus===0) { frm.add_custom_button(__('Sales Order'), () => frm.events.get_items_from_sales_order(frm), - __("Get items from")); + __("Get Items From")); } if (frm.doc.docstatus == 1 && frm.doc.status == 'Stopped') { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index c504e23677..8ee775e161 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -136,7 +136,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend company: me.frm.doc.company } }) - }, __("Get items from")); + }, __("Get Items From")); } if(this.frm.doc.docstatus == 1 && this.frm.doc.status!="Closed") { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 39fd029a89..32923353ee 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -225,7 +225,7 @@ frappe.ui.form.on('Stock Entry', { docstatus: 1 } }) - }, __("Get items from")); + }, __("Get Items From")); frm.add_custom_button(__('Material Request'), function() { erpnext.utils.map_current_doc({ @@ -240,7 +240,7 @@ frappe.ui.form.on('Stock Entry', { status: ["not in", ["Transferred", "Issued"]] } }) - }, __("Get items from")); + }, __("Get Items From")); } if (frm.doc.docstatus===0 && frm.doc.purpose == "Material Issue") { frm.add_custom_button(__('Expired Batches'), function() { @@ -263,7 +263,7 @@ frappe.ui.form.on('Stock Entry', { } } }); - }, __("Get items from")); + }, __("Get Items From")); } frm.events.show_bom_custom_button(frm); @@ -282,7 +282,7 @@ frappe.ui.form.on('Stock Entry', { }, stock_entry_type: function(frm){ - frm.remove_custom_button('Bill of Materials', "Get items from"); + frm.remove_custom_button('Bill of Materials', "Get Items From"); frm.events.show_bom_custom_button(frm); frm.trigger('add_to_transit'); }, @@ -427,7 +427,7 @@ frappe.ui.form.on('Stock Entry', { ['Material Issue', 'Material Receipt', 'Material Transfer', 'Send to Subcontractor'].includes(frm.doc.purpose)) { frm.add_custom_button(__('Bill of Materials'), function() { frm.events.get_items_from_bom(frm); - }, __("Get items from")); + }, __("Get Items From")); } }, From 08e5031be2d41f1d2e42fe9e3a58659c19f3beea Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 30 Oct 2020 17:36:20 +0530 Subject: [PATCH 081/229] chore: Supplier Quotation Get Items From cleanup --- .../doctype/supplier_quotation/supplier_quotation.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index ab96973ced..a7cab5015e 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -37,13 +37,15 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext source_doctype: "Material Request", target: me.frm, setters: { - company: me.frm.doc.company + schedule_date: undefined, + status: undefined }, get_query_filters: { material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99] + per_ordered: ["<", 99.99], + company: me.frm.doc.company } }) }, __("Get Items From")); @@ -58,11 +60,11 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext source_doctype: "Request for Quotation", target: me.frm, setters: { - company: me.frm.doc.company, transaction_date: null }, get_query_filters: { - supplier: me.frm.doc.supplier + supplier: me.frm.doc.supplier, + company: me.frm.doc.company }, get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier" From 66b2b544f88764a394f37e523db4aef98bf8d759 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 30 Oct 2020 17:45:28 +0530 Subject: [PATCH 082/229] chore: Purchase Receipt, added required by date in PO popup --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 8ee775e161..bc1d81d356 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -128,6 +128,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend target: me.frm, setters: { supplier: me.frm.doc.supplier, + schedule_date: undefined }, get_query_filters: { docstatus: 1, From 97d38548aa6ba48e731c31ec80c0dfae84f9d8ce Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 30 Oct 2020 17:50:25 +0530 Subject: [PATCH 083/229] chore: Purchase Invoice Get Items From cleanup --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 62213abaa3..1d41d0fa2a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -99,6 +99,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ target: me.frm, setters: { supplier: me.frm.doc.supplier || undefined, + schedule_date: undefined }, get_query_filters: { docstatus: 1, @@ -114,9 +115,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice", source_doctype: "Purchase Receipt", target: me.frm, - date_field: "posting_date", setters: { supplier: me.frm.doc.supplier || undefined, + posting_date: undefined }, get_query_filters: { docstatus: 1, From 542ae6ab97f1ffbc873e80cc492b88e14a7b3c92 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 2 Nov 2020 08:50:48 +0530 Subject: [PATCH 084/229] chore: Linter and Sider fixes - Translatable strings cannot be multi line or be formatted by '+' - Reduce no of tabs in stock_entry.js --- erpnext/buying/doctype/purchase_order/purchase_order.js | 2 +- erpnext/stock/doctype/material_request/material_request.js | 3 +-- erpnext/stock/doctype/stock_entry/stock_entry.js | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 3c6ebd6604..2f52a9e035 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -299,7 +299,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if(me.values) { me.values.sub_con_rm_items.map((row,i) => { if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { - frappe.throw(__("Item Code, warehouse, quantity are required on row" + (i+1))); + frappe.throw(__("Item Code, warehouse, quantity are required on row {0}", [i+1])); } }) me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index f004e9145a..01edd99e9d 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -281,8 +281,7 @@ frappe.ui.form.on('Material Request', { fieldname:'default_supplier', fieldtype: 'Link', options: 'Supplier', - description: __('Select a Supplier from the Default Suppliers of the items below. \ - On selection, a Purchase Order will be made against items belonging to the selected Supplier only.'), + description: __('Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.'), get_query: () => { return{ query: "erpnext.stock.doctype.material_request.material_request.get_default_supplier_query", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 32923353ee..91217582ca 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -425,9 +425,9 @@ frappe.ui.form.on('Stock Entry', { show_bom_custom_button: function(frm){ if (frm.doc.docstatus === 0 && ['Material Issue', 'Material Receipt', 'Material Transfer', 'Send to Subcontractor'].includes(frm.doc.purpose)) { - frm.add_custom_button(__('Bill of Materials'), function() { - frm.events.get_items_from_bom(frm); - }, __("Get Items From")); + frm.add_custom_button(__('Bill of Materials'), function() { + frm.events.get_items_from_bom(frm); + }, __("Get Items From")); } }, From f116fbf84d9e4ec6c6d8694463faa96d0e7c1ee0 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 2 Nov 2020 10:27:54 +0530 Subject: [PATCH 085/229] refactor(UAE VAT 201): Update erpnext/regional/report/uae_vat_201/test_uae_vat_201.py Co-authored-by: Shivam Mishra --- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 3037a07698..70c6b74f5e 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -15,7 +15,7 @@ from erpnext.regional.report.uae_vat_201.uae_vat_201 import ( get_exempt_total, get_standard_rated_expenses_total, get_standard_rated_expenses_tax, - ) +) test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] @@ -295,4 +295,4 @@ def create_purchase_invoices(): pi.recoverable_standard_rated_expenses = 1 - pi.submit() \ No newline at end of file + pi.submit() From be7a4c8b0aaf5d33e23bbb6f635f47484425637d Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 2 Nov 2020 10:28:14 +0530 Subject: [PATCH 086/229] refactor(UAE VAT 201): Update erpnext/regional/report/uae_vat_201/test_uae_vat_201.py Co-authored-by: Shivam Mishra --- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 70c6b74f5e..84db9146cc 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -95,8 +95,10 @@ def set_vat_accounts(): fields=["name"], filters = { "company": "_Test Company UAE VAT", - "is_group":0, - "account_type": "Tax"}) + "is_group": 0, + "account_type": "Tax" + } + ) uae_vat_accounts = [] for d in vat_accounts: From 46265d3fc7e1101cea4960e5b593b393ada9deb7 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 2 Nov 2020 10:28:41 +0530 Subject: [PATCH 087/229] refactor(UAE VAT 201): Update erpnext/regional/report/uae_vat_201/test_uae_vat_201.py Co-authored-by: Shivam Mishra --- .../regional/report/uae_vat_201/test_uae_vat_201.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 84db9146cc..7514652894 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -48,12 +48,12 @@ class TestUaeVat201(TestCase): filters = {"company": "_Test Company UAE VAT"} total_emiratewise = get_total_emiratewise(filters) amounts_by_emirate = {} - for d in total_emiratewise: - emirate, amount, vat= d + for data in total_emiratewise: + emirate, amount, vat = data amounts_by_emirate[emirate] = { - "raw_amount": amount, - "raw_vat_amount": vat, - } + "raw_amount": amount, + "raw_vat_amount": vat, + } self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],300) self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) From 7b771d139c66ce2251941349ffb4e4db88ce47c9 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 2 Nov 2020 10:29:14 +0530 Subject: [PATCH 088/229] refactor(UAE VAT 201): Update erpnext/regional/report/uae_vat_201/test_uae_vat_201.py Co-authored-by: Shivam Mishra --- erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 7514652894..5fdb2645f7 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -101,12 +101,11 @@ def set_vat_accounts(): ) uae_vat_accounts = [] - for d in vat_accounts: - uae_vat_accounts.append( - { + for account in vat_accounts: + uae_vat_accounts.append({ "doctype": "UAE VAT Account", - "account":d.name - }) + "account": account.name + }) frappe.get_doc({ "company": "_Test Company UAE VAT", From d322181fd7d2d6cce9301b41094b314803ce7973 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 2 Nov 2020 10:29:38 +0530 Subject: [PATCH 089/229] refactor(UAE VAT 201): Update erpnext/regional/report/uae_vat_201/uae_vat_201.py Co-authored-by: Shivam Mishra --- .../regional/report/uae_vat_201/uae_vat_201.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 216762bc8d..201ddf63f5 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -65,14 +65,14 @@ def get_chart(emirates, amounts_by_emirate): datasets.append({'name': _('Vat Amount (AED)'), 'values': vat_amount}) chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } + "type": "bar", + "fieldtype": "Currency" + "data": { + 'labels': labels, + 'datasets': datasets } - - chart["type"] = "bar" - chart["fieldtype"] = "Currency" + } + return chart def append_vat_on_sales(data, filters): @@ -333,4 +333,4 @@ def get_conditions(filters): ("to_date", " and posting_date<=%(to_date)s")): if filters.get(opts[0]): conditions += opts[1] - return conditions \ No newline at end of file + return conditions From c53237f5e0177a5e2a5aef39e476a58077067e9b Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 2 Nov 2020 11:02:41 +0530 Subject: [PATCH 090/229] refactor(UAE VAT 201): solve syntax errors --- .../report/uae_vat_201/uae_vat_201.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 201ddf63f5..f641e652a6 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -66,13 +66,13 @@ def get_chart(emirates, amounts_by_emirate): chart = { "type": "bar", - "fieldtype": "Currency" + "fieldtype": "Currency", "data": { 'labels': labels, 'datasets': datasets } } - + return chart def append_vat_on_sales(data, filters): @@ -89,14 +89,14 @@ def append_vat_on_sales(data, filters): frappe.format(get_reverse_charge_total(filters), 'Currency'), frappe.format(get_reverse_charge_tax(filters), 'Currency')) - append_data(data, '4', _('Zero Rated'), + append_data(data, '4', _('Zero Rated'), frappe.format(get_zero_rated_total(filters), 'Currency'), "-") append_data(data, '5', _('Exempt Supplies'), frappe.format(get_exempt_total(filters), 'Currency'),"-") append_data(data, '', '', '', '') - + return emirates, amounts_by_emirate def standard_rated_expenses_emiratewise(data, filters): @@ -159,7 +159,7 @@ def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" query_filters = get_filters(filters) query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Sales Invoice', + return frappe.db.get_list('Sales Invoice', filters = query_filters, fields = ['vat_emirate as emirate','sum(total)', 'sum(total_taxes_and_charges)'], group_by='vat_emirate', @@ -194,7 +194,7 @@ def get_reverse_charge_total(filters): query_filters = get_filters(filters) query_filters['reverse_charge'] = ['=', 'Y'] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', + return frappe.db.get_list('Purchase Invoice', filters = query_filters, fields = ['sum(total)'], as_list=True, @@ -222,7 +222,7 @@ def get_reverse_charge_recoverable_total(filters): query_filters['reverse_charge'] = ['=', 'Y'] query_filters['recoverable_reverse_charge'] = ['>', '0'] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', + return frappe.db.get_list('Purchase Invoice', filters = query_filters, fields = ['sum(total)'], as_list=True, @@ -260,7 +260,7 @@ def get_standard_rated_expenses_total(filters): query_filters = get_filters(filters) query_filters['recoverable_standard_rated_expenses'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', + return frappe.db.get_list('Purchase Invoice', filters = query_filters, fields = ['sum(total)'], as_list=True, @@ -272,7 +272,7 @@ def get_standard_rated_expenses_tax(filters): query_filters = get_filters(filters) query_filters['recoverable_standard_rated_expenses'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', + return frappe.db.get_list('Purchase Invoice', filters = query_filters, fields = ['sum(recoverable_standard_rated_expenses)'], as_list=True, @@ -284,7 +284,7 @@ def get_tourist_tax_return_total(filters): query_filters = get_filters(filters) query_filters['tourist_tax_return'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Sales Invoice', + return frappe.db.get_list('Sales Invoice', filters = query_filters, fields = ['sum(total)'], as_list=True, @@ -296,7 +296,7 @@ def get_tourist_tax_return_tax(filters): query_filters = get_filters(filters) query_filters['tourist_tax_return'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Sales Invoice', + return frappe.db.get_list('Sales Invoice', filters = query_filters, fields = ['sum(tourist_tax_return)'], as_list=True, From b0eca23485b9319754e33af11727605df2f97c82 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 2 Nov 2020 12:47:10 +0530 Subject: [PATCH 091/229] Refactor(UAE VAT 201): Fix fomatiing --- .../report/uae_vat_201/test_uae_vat_201.py | 184 +++++--------- .../report/uae_vat_201/uae_vat_201.py | 227 ++++++++++-------- .../regional/united_arab_emirates/utils.py | 22 +- 3 files changed, 201 insertions(+), 232 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 5fdb2645f7..b0a21de613 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -8,8 +8,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse_account from erpnext.regional.report.uae_vat_201.uae_vat_201 import ( - get_total_emiratewise, - get_tourist_tax_return_total, + get_total_emiratewise, + get_tourist_tax_return_total, get_tourist_tax_return_tax, get_zero_rated_total, get_exempt_total, @@ -26,7 +26,6 @@ class TestUaeVat201(TestCase): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company UAE VAT'") frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company UAE VAT'") - make_company("_Test Company UAE VAT", "_TCUV") set_vat_accounts() @@ -79,7 +78,6 @@ def make_company(company_name, abbr): else: company = frappe.get_doc("Company", company_name) - # indempotent company.create_default_warehouses() if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): @@ -95,7 +93,7 @@ def set_vat_accounts(): fields=["name"], filters = { "company": "_Test Company UAE VAT", - "is_group": 0, + "is_group": 0, "account_type": "Tax" } ) @@ -103,7 +101,7 @@ def set_vat_accounts(): uae_vat_accounts = [] for account in vat_accounts: uae_vat_accounts.append({ - "doctype": "UAE VAT Account", + "doctype": "UAE VAT Account", "account": account.name }) @@ -125,7 +123,6 @@ def make_customer(): customer = frappe.get_doc("Customer", "_Test UAE Customer") def make_supplier(): - if not frappe.db.exists("Supplier", "_Test UAE Supplier"): frappe.get_doc({ "supplier_group": "Local", @@ -140,15 +137,15 @@ def create_warehouse(warehouse_name, properties=None, company=None): warehouse_id = erpnext.encode_company_abbr(warehouse_name, company) if not frappe.db.exists("Warehouse", warehouse_id): - w = frappe.new_doc("Warehouse") - w.warehouse_name = warehouse_name - w.parent_warehouse = "All Warehouses - _TCUV" - w.company = company - w.account = get_warehouse_account(warehouse_name, company) + warehouse = frappe.new_doc("Warehouse") + warehouse.warehouse_name = warehouse_name + warehouse.parent_warehouse = "All Warehouses - _TCUV" + warehouse.company = company + warehouse.account = get_warehouse_account(warehouse_name, company) if properties: - w.update(properties) - w.save() - return w.name + warehouse.update(properties) + warehouse.save() + return warehouse.name else: return warehouse_id @@ -166,13 +163,15 @@ def make_item(item_code, properties=None): if properties: item.update(properties) - + item.insert() return item def make_sales_invoices(): - si = create_sales_invoice(company="_Test Company UAE VAT", + def make_sales_invoices_wrapper(emirate, item, tax = True, tourist_tax= False): + si = create_sales_invoice( + company="_Test Company UAE VAT", customer = '_Test UAE Customer', currency = 'AED', warehouse = 'Finished Goods - _TCUV', @@ -180,119 +179,60 @@ def make_sales_invoices(): income_account = 'Sales - _TCUV', expense_account = 'Cost of Goods Sold - _TCUV', cost_center = 'Main - _TCUV', - sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", - item = "_Test UAE VAT Item", + item = item, do_not_save=1 ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) - si.vat_emirate = 'Dubai' - si.submit() + si.vat_emirate = emirate + if tax: + si.append( + "taxes", { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + } + ) + if tourist_tax: + si.tourist_tax_return = 2 + si.submit() - si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", - item = "_Test UAE VAT Item", - do_not_save=1 - ) - si.vat_emirate = 'Sharjah' - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) - si.submit() + #Define Item Names + uae_item = "_Test UAE VAT Item" + uae_exempt_item = "_Test UAE VAT Exempt Item" + uae_zero_rated_item = "_Test UAE VAT Zero Rated Item" - si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", - item = "_Test UAE VAT Item", - do_not_save=1 - ) - - si.tourist_tax_return = 2 - - si.vat_emirate = 'Dubai' - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) - si.submit() - - si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", - item = "_Test UAE VAT Zero Rated Item", - do_not_save=1 - ) - si.vat_emirate = 'Sharjah' - si.submit() - - si = create_sales_invoice(company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - sales_taxes_and_charges_template = "UAE VAT 5% - _TCUV", - item = "_Test UAE VAT Exempt Item", - do_not_save=1 - ) - si.vat_emirate = 'Sharjah' - si.submit() + #Sales Invoice with standard rated expense in Dubai + make_sales_invoices_wrapper('Dubai', uae_item) + #Sales Invoice with standard rated expense in Sharjah + make_sales_invoices_wrapper('Sharjah', uae_item) + #Sales Invoice with Tourist Tax Return + make_sales_invoices_wrapper('Dubai', uae_item, True, True) + #Sales Invoice with Exempt Item + make_sales_invoices_wrapper('Sharjah', uae_exempt_item, False) + #Sales Invoice with Zero Rated Item + make_sales_invoices_wrapper('Sharjah', uae_zero_rated_item, False) def create_purchase_invoices(): - pi = make_purchase_invoice( - company="_Test Company UAE VAT", - supplier = '_Test UAE Supplier', - supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - currency = 'AED', - cost_center = 'Main - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - item = "_Test UAE VAT Item", - do_not_save=1, - uom = "Nos" - ) + company="_Test Company UAE VAT", + supplier = '_Test UAE Supplier', + supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', + warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', + currency = 'AED', + cost_center = 'Main - _TCUV', + expense_account = 'Cost of Goods Sold - _TCUV', + item = "_Test UAE VAT Item", + do_not_save=1, + uom = "Nos" + ) pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0 + }) pi.recoverable_standard_rated_expenses = 1 diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index f641e652a6..78dcb367a4 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -54,11 +54,11 @@ def get_chart(emirates, amounts_by_emirate): labels = [] amount = [] vat_amount = [] - for d in emirates: - if d in amounts_by_emirate: - amount.append(amounts_by_emirate[d]["raw_amount"]) - vat_amount.append(amounts_by_emirate[d]["raw_vat_amount"]) - labels.append(d) + for emirate in emirates: + if emirate in amounts_by_emirate: + amount.append(amounts_by_emirate[emirate]["raw_amount"]) + vat_amount.append(amounts_by_emirate[emirate]["raw_vat_amount"]) + labels.append(emirate) datasets = [] datasets.append({'name': _('Amount (AED)'), 'values': amount}) @@ -81,7 +81,8 @@ def append_vat_on_sales(data, filters): emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) - append_data(data, '2', _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), + append_data(data, '2', + _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency')) @@ -104,37 +105,26 @@ def standard_rated_expenses_emiratewise(data, filters): total_emiratewise = get_total_emiratewise(filters) emirates = get_emirates() amounts_by_emirate = {} - for d in total_emiratewise: - emirate, amount, vat= d + for emirate, amount, vat in total_emiratewise: amounts_by_emirate[emirate] = { - "legend": emirate, - "raw_amount": amount, - "raw_vat_amount": vat, - "amount": frappe.format(amount, 'Currency'), - "vat_amount": frappe.format(vat, 'Currency'), - } + "legend": emirate, + "raw_amount": amount, + "raw_vat_amount": vat, + "amount": frappe.format(amount, 'Currency'), + "vat_amount": frappe.format(vat, 'Currency'), + } amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate) - - for d, emirate in enumerate(emirates, 97): - if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(d)) - amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) - data.append(amounts_by_emirate[emirate]) - else: - append_data(data, _('1{0}').format(chr(d)), - _('Standard rated supplies in {0}').format(emirate), - frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) return emirates, amounts_by_emirate def append_emiratewise_expenses(data, emirates, amounts_by_emirate): """Append emiratewise standard rated expenses and vat.""" - for d, emirate in enumerate(emirates, 97): + for no, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(d)) + amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(no)) amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) data.append(amounts_by_emirate[emirate]) else: - append_data(data, _('1{0}').format(chr(d)), + append_data(data, _('1{0}').format(chr(no)), _('Standard rated supplies in {0}').format(emirate), frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) return amounts_by_emirate @@ -145,11 +135,9 @@ def append_vat_on_expenses(data, filters): append_data(data, '9', _('Standard Rated Expenses'), frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), frappe.format(get_standard_rated_expenses_tax(filters), 'Currency')) - append_data(data, '10', _('Supplies subject to the reverse charge provision'), frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), - frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency') -) + frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency')) def append_data(data, no, legend, amount, vat_amount): """Returns data with appended value.""" @@ -159,7 +147,7 @@ def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" query_filters = get_filters(filters) query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Sales Invoice', + return frappe.db.get_all('Sales Invoice', filters = query_filters, fields = ['vat_emirate as emirate','sum(total)', 'sum(total_taxes_and_charges)'], group_by='vat_emirate', @@ -194,26 +182,30 @@ def get_reverse_charge_total(filters): query_filters = get_filters(filters) query_filters['reverse_charge'] = ['=', 'Y'] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + try: + return frappe.db.get_all('Purchase Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" select sum(debit) from - `tabPurchase Invoice` p inner join `tabGL Entry` gl - on gl.voucher_no = p.name + `tabPurchase Invoice` p inner join `tabGL Entry` gl + on + gl.voucher_no = p.name where - p.reverse_charge = "Y" - and p.docstatus = 1 - and gl.docstatus = 1 - and account in (select account from `tabUAE VAT Account` where parent=%(company)s) - {where_conditions} ; + p.reverse_charge = "Y" + and p.docstatus = 1 + and gl.docstatus = 1 + and account in (select account from `tabUAE VAT Account` where parent=%(company)s) + {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 def get_reverse_charge_recoverable_total(filters): @@ -222,27 +214,33 @@ def get_reverse_charge_recoverable_total(filters): query_filters['reverse_charge'] = ['=', 'Y'] query_filters['recoverable_reverse_charge'] = ['>', '0'] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + try: + return frappe.db.get_all('Purchase Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) return frappe.db.sql(""" - select sum(debit * p.recoverable_reverse_charge / 100) from - `tabPurchase Invoice` p inner join `tabGL Entry` gl - on gl.voucher_no = p.name + select + sum(debit * p.recoverable_reverse_charge / 100) + from + `tabPurchase Invoice` p inner join `tabGL Entry` gl + on + gl.voucher_no = p.name where - p.reverse_charge = "Y" - and p.docstatus = 1 - and p.recoverable_reverse_charge > 0 - and gl.docstatus = 1 - and account in (select account from `tabUAE VAT Account` where parent=%(company)s) - {where_conditions} ; + p.reverse_charge = "Y" + and p.docstatus = 1 + and p.recoverable_reverse_charge > 0 + and gl.docstatus = 1 + and account in (select account from `tabUAE VAT Account` where parent=%(company)s) + {where_conditions} ; """.format(where_conditions=conditions), filters)[0][0] or 0 def get_conditions_join(filters): @@ -251,8 +249,8 @@ def get_conditions_join(filters): for opts in (("company", " and p.company=%(company)s"), ("from_date", " and p.posting_date>=%(from_date)s"), ("to_date", " and p.posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + if filters.get(opts[0]): + conditions += opts[1] return conditions def get_standard_rated_expenses_total(filters): @@ -260,77 +258,102 @@ def get_standard_rated_expenses_total(filters): query_filters = get_filters(filters) query_filters['recoverable_standard_rated_expenses'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + try: + return frappe.db.get_all('Purchase Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" query_filters = get_filters(filters) query_filters['recoverable_standard_rated_expenses'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Purchase Invoice', - filters = query_filters, - fields = ['sum(recoverable_standard_rated_expenses)'], - as_list=True, - limit = 1 - )[0][0] or 0 + try: + return frappe.db.get_all('Purchase Invoice', + filters = query_filters, + fields = ['sum(recoverable_standard_rated_expenses)'], + as_list=True, + limit = 1 + )[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) query_filters['tourist_tax_return'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Sales Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + try: + return frappe.db.get_all('Sales Invoice', + filters = query_filters, + fields = ['sum(total)'], + as_list=True, + limit = 1 + )[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) query_filters['tourist_tax_return'] = ['>', 0] query_filters['docstatus'] = ['=', 1] - return frappe.db.get_list('Sales Invoice', - filters = query_filters, - fields = ['sum(tourist_tax_return)'], - as_list=True, - limit = 1 - )[0][0] or 0 + try: + return frappe.db.get_all('Sales Invoice', + filters = query_filters, + fields = ['sum(tourist_tax_return)'], + as_list=True, + limit = 1 + )[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(i.base_amount) as total from - `tabSales Invoice Item` i inner join `tabSales Invoice` s - on i.parent = s.name - where s.docstatus = 1 and i.is_zero_rated = 1 - {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + try: + return frappe.db.sql(""" + select + sum(i.base_amount) as total + from + `tabSales Invoice Item` i inner join `tabSales Invoice` s + on + i.parent = s.name + where + s.docstatus = 1 and i.is_zero_rated = 1 + {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_exempt_total(filters): """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt.""" conditions = get_conditions(filters) - return frappe.db.sql(""" - select sum(i.base_amount) as total from - `tabSales Invoice Item` i inner join `tabSales Invoice` s - on i.parent = s.name - where s.docstatus = 1 and i.is_exempt = 1 - {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 - + try: + return frappe.db.sql(""" + select + sum(i.base_amount) as total + from + `tabSales Invoice Item` i inner join `tabSales Invoice` s + on + i.parent = s.name + where + s.docstatus = 1 and i.is_exempt = 1 + {where_conditions} ; + """.format(where_conditions=conditions), filters)[0][0] or 0 + except (IndexError, TypeError): + return 0 def get_conditions(filters): """The conditions to be used to filter data to calculate the total sale.""" conditions = "" for opts in (("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + if filters.get(opts[0]): + conditions += opts[1] return conditions diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 7430db4fce..872839495a 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -35,7 +35,12 @@ def get_account_currency(account): if not account: return def generator(): - account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"]) + account_currency, company = frappe.get_cached_value( + "Account", + account, + ["account_currency", + "company"] + ) if not account_currency: account_currency = frappe.get_cached_value('Company', company, "default_currency") @@ -53,8 +58,8 @@ def get_tax_accounts(company): if not tax_accounts_list and not frappe.flags.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) - for d in tax_accounts_list: - for key, name in d.items(): + for tax_account in tax_accounts_list: + for _, name in tax_account.items(): tax_accounts_dict[name] = name return tax_accounts_dict @@ -131,8 +136,7 @@ def make_gl_entry(tax, gl_entries, doc, tax_accounts): if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: account_currency = get_account_currency(tax.account_head) - gl_entries.append(doc.get_gl_dict( - { + gl_entries.append(doc.get_gl_dict({ "account": tax.account_head, "cost_center": tax.cost_center, "posting_date": doc.posting_date, @@ -141,8 +145,8 @@ def make_gl_entry(tax, gl_entries, doc, tax_accounts): dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ if account_currency==doc.company_currency \ else tax.tax_amount_after_discount_amount - }, account_currency, item=tax) - ) + }, account_currency, item=tax + )) return gl_entries @@ -152,4 +156,6 @@ def validate_returns(doc, method): if country != 'United Arab Emirates': return if doc.reverse_charge == 'Y' and flt(doc.recoverable_standard_rated_expenses) != 0: - frappe.throw(_("Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y")) \ No newline at end of file + frappe.throw(_( + "Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y" + )) From 7a034595fe7c2f60090e9bbdc185a98ecd0f4f38 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 2 Nov 2020 13:12:27 +0530 Subject: [PATCH 092/229] fix(UAE VAT 201): Remove Chart --- .../report/uae_vat_201/uae_vat_201.py | 29 +------------------ .../regional/united_arab_emirates/utils.py | 2 +- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 78dcb367a4..c151c622a6 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -8,8 +8,7 @@ from frappe import _ def execute(filters=None): columns = get_columns() data, emirates, amounts_by_emirate = get_data(filters) - chart = get_chart(emirates, amounts_by_emirate) - return columns, data, None, chart + return columns, data def get_columns(): """Creates a list of dictionaries that are used to generate column headers of the data table.""" @@ -49,32 +48,6 @@ def get_data(filters = None): append_vat_on_expenses(data, filters) return data, emirates, amounts_by_emirate -def get_chart(emirates, amounts_by_emirate): - """Returns chart data.""" - labels = [] - amount = [] - vat_amount = [] - for emirate in emirates: - if emirate in amounts_by_emirate: - amount.append(amounts_by_emirate[emirate]["raw_amount"]) - vat_amount.append(amounts_by_emirate[emirate]["raw_vat_amount"]) - labels.append(emirate) - - datasets = [] - datasets.append({'name': _('Amount (AED)'), 'values': amount}) - datasets.append({'name': _('Vat Amount (AED)'), 'values': vat_amount}) - - chart = { - "type": "bar", - "fieldtype": "Currency", - "data": { - 'labels': labels, - 'datasets': datasets - } - } - - return chart - def append_vat_on_sales(data, filters): """Appends Sales and All Other Outputs.""" append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '') diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 872839495a..7d5fd6ecf8 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -59,7 +59,7 @@ def get_tax_accounts(company): if not tax_accounts_list and not frappe.flags.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) for tax_account in tax_accounts_list: - for _, name in tax_account.items(): + for account, name in tax_account.items(): tax_accounts_dict[name] = name return tax_accounts_dict From a2ba8ea74a39403d9cd2fd2cc2af895059d9cdf8 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Tue, 3 Nov 2020 10:57:52 +0800 Subject: [PATCH 093/229] style: Clean up code formatting --- .../stock/doctype/item_price/item_price.py | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 54f534f3d1..bed5ea9ab6 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -46,12 +46,7 @@ class ItemPrice(Document): self.item_name, self.item_description = frappe.db.get_value("Item", self.item_code,["item_name", "description"]) def check_duplicates(self): - conditions = """ - where - item_code = %(item_code)s - and price_list = %(price_list)s - and name != %(name)s - """ + conditions = """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" for field in [ "uom", @@ -59,23 +54,18 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier", - ]: + "supplier",]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: conditions += "and (isnull({0}) or {0} = '')".format(field) - price_list_rate = frappe.db.sql( - """ + price_list_rate = frappe.db.sql(""" select price_list_rate from `tabItem Price` {conditions} - """.format( - conditions=conditions - ), - self.as_dict(), - ) + """.format(conditions=conditions), + self.as_dict(),) if price_list_rate: frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) From 846ff323e2ef5853b3fe1af614b9589de93df6e2 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 3 Nov 2020 17:35:44 +0530 Subject: [PATCH 094/229] feat: sales order status filter for production plan --- .../doctype/production_plan/production_plan.json | 10 +++++++++- .../doctype/production_plan/production_plan.py | 6 ++++-- .../doctype/production_plan/test_production_plan.py | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 850d5aeff8..63df5f379d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -19,6 +19,7 @@ "column_break2", "from_date", "to_date", + "sales_order_status", "sales_orders_detail", "get_sales_orders", "sales_orders", @@ -301,13 +302,20 @@ "label": "Warehouses", "options": "Production Plan Material Request Warehouse", "read_only": 1 + }, + { + "depends_on": "eval: doc.get_items_from == \"Sales Order\"", + "fieldname": "sales_order_status", + "fieldtype": "Select", + "label": "Sales Order Status", + "options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-26 13:00:54.335319", + "modified": "2020-11-03 15:19:18.270529", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a314a15c23..3833e86d27 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -571,6 +571,8 @@ def get_sales_orders(self): so_filter += " and so.customer = %(customer)s" if self.project: so_filter += " and so.project = %(project)s" + if self.sales_order_status: + so_filter += "and so.status = %(sales_order_status)s" if self.item_code: item_filter += " and so_item.item_code = %(item)s" @@ -594,8 +596,8 @@ def get_sales_orders(self): "customer": self.customer, "project": self.project, "item": self.item_code, - "company": self.company - + "company": self.company, + "sales_order_status": self.sales_order_status }, as_dict=1) return open_so diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index fa9d080cca..27335aa204 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -137,7 +137,8 @@ class TestProductionPlan(unittest.TestCase): 'from_date': so.transaction_date, 'to_date': so.transaction_date, 'customer': so.customer, - 'item_code': item + 'item_code': item, + 'sales_order_status': so.status }) sales_orders = get_sales_orders(pln) or {} sales_orders = [d.get('name') for d in sales_orders if d.get('name') == sales_order] From 574477187fca95792d8c8b6baae8143bd2db11bc Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 3 Nov 2020 20:22:48 +0530 Subject: [PATCH 095/229] chore: Pricing Rule with Item Price Test --- .../doctype/pricing_rule/test_pricing_rule.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 22a031c162..ec0a485bfc 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -484,6 +484,43 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") + def test_item_price_with_pricing_rule(self): + item = make_item("Water Flask") + make_item_price("Water Flask", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "items": [{ + "item_code": "Water Flask", + }], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 0, + "margin_type": "Percentage", + "margin_rate_or_amount": 2, + "company": "_Test Company" + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code="Water Flask") + si.selling_price_list = "_Test Price List" + si.save() + + # If rate in Rule is 0, give preference to Item Price if it exists + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].margin_rate_or_amount, 2) + self.assertEqual(si.items[0].rate_with_margin, 102) + self.assertEqual(si.items[0].rate, 102) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() + item.delete() + def make_pricing_rule(**args): args = frappe._dict(args) From f2c895ddb5b9d7b5b8f07e1d590e07ff5a745b7c Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 4 Nov 2020 10:31:37 +0530 Subject: [PATCH 096/229] feat(uae vat 201): do not take zero and nil rated items in standard rated expense emiratewise --- .../report/uae_vat_201/test_uae_vat_201.py | 2 +- .../report/uae_vat_201/uae_vat_201.py | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index b0a21de613..daa69768c5 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -53,7 +53,7 @@ class TestUaeVat201(TestCase): "raw_amount": amount, "raw_vat_amount": vat, } - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],300) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],100) self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index c151c622a6..f2d5a5e113 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -118,14 +118,23 @@ def append_data(data, no, legend, amount, vat_amount): def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" - query_filters = get_filters(filters) - query_filters['docstatus'] = ['=', 1] - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['vat_emirate as emirate','sum(total)', 'sum(total_taxes_and_charges)'], - group_by='vat_emirate', - as_list=True - ) + conditions = get_conditions(filters) + try: + return frappe.db.sql(""" + select + s.vat_emirate as emirate, sum(i.base_amount) as total, sum(s.total_taxes_and_charges) + from + `tabSales Invoice Item` i inner join `tabSales Invoice` s + on + i.parent = s.name + where + s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 + {where_conditions} + group by + s.vat_emirate; + """.format(where_conditions=conditions), filters) + except (IndexError, TypeError): + return 0 def get_emirates(): """Returns a List of emirates in the order that they are to be displayed.""" From bed4e85aec2b8475ab817916f0d357c6e977fe55 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Wed, 4 Nov 2020 10:44:22 +0530 Subject: [PATCH 097/229] fix(uae vat 201): date filters were not working --- .../report/uae_vat_201/uae_vat_201.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index f2d5a5e113..de727d2ecd 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -150,20 +150,20 @@ def get_emirates(): def get_filters(filters): """The conditions to be used to filter data to calculate the total sale.""" - query_filters = {} + query_filters = [] if filters.get("company"): - query_filters["company"] = ['=', filters['company']] + query_filters.append(["company", '=', filters['company']]) if filters.get("from_date"): - query_filters["posting_date"] = ['>=', filters['from_date']] + query_filters.append(["posting_date", '>=', filters['from_date']]) if filters.get("from_date"): - query_filters["posting_date"] = ['<=', filters['to_date']] + query_filters.append(["posting_date", '<=', filters['to_date']]) return query_filters def get_reverse_charge_total(filters): """Returns the sum of the total of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters['reverse_charge'] = ['=', 'Y'] - query_filters['docstatus'] = ['=', 1] + query_filters.append(['reverse_charge', '=', 'Y']) + query_filters.append(['docstatus', '=', 1]) try: return frappe.db.get_all('Purchase Invoice', filters = query_filters, @@ -193,9 +193,9 @@ def get_reverse_charge_tax(filters): def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters['reverse_charge'] = ['=', 'Y'] - query_filters['recoverable_reverse_charge'] = ['>', '0'] - query_filters['docstatus'] = ['=', 1] + query_filters.append(['reverse_charge', '=', 'Y']) + query_filters.append(['recoverable_reverse_charge', '>', '0']) + query_filters.append(['docstatus', '=', 1]) try: return frappe.db.get_all('Purchase Invoice', filters = query_filters, @@ -238,8 +238,8 @@ def get_conditions_join(filters): def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters['recoverable_standard_rated_expenses'] = ['>', 0] - query_filters['docstatus'] = ['=', 1] + query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) + query_filters.append(['docstatus', '=', 1]) try: return frappe.db.get_all('Purchase Invoice', filters = query_filters, @@ -253,8 +253,8 @@ def get_standard_rated_expenses_total(filters): def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters['recoverable_standard_rated_expenses'] = ['>', 0] - query_filters['docstatus'] = ['=', 1] + query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) + query_filters.append(['docstatus', '=', 1]) try: return frappe.db.get_all('Purchase Invoice', filters = query_filters, @@ -268,8 +268,8 @@ def get_standard_rated_expenses_tax(filters): def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters['tourist_tax_return'] = ['>', 0] - query_filters['docstatus'] = ['=', 1] + query_filters.append(['tourist_tax_return', '>', 0]) + query_filters.append(['docstatus', '=', 1]) try: return frappe.db.get_all('Sales Invoice', filters = query_filters, @@ -283,8 +283,8 @@ def get_tourist_tax_return_total(filters): def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters['tourist_tax_return'] = ['>', 0] - query_filters['docstatus'] = ['=', 1] + query_filters.append(['tourist_tax_return', '>', 0]) + query_filters.append(['docstatus', '=', 1]) try: return frappe.db.get_all('Sales Invoice', filters = query_filters, @@ -300,7 +300,7 @@ def get_zero_rated_total(filters): conditions = get_conditions(filters) try: return frappe.db.sql(""" - select + select sum(i.base_amount) as total from `tabSales Invoice Item` i inner join `tabSales Invoice` s From 09b8ca0ddba12ef704c38ae512fb64c7ca1c87b3 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 4 Nov 2020 14:46:34 +0530 Subject: [PATCH 098/229] fix: list index out of range on incilding uom --- erpnext/stock/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 11e758fce3..f9ac25443e 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -288,7 +288,6 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto return convertible_cols = {} - is_dict_obj = False if isinstance(result[0], dict): is_dict_obj = True @@ -310,13 +309,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto for row_idx, row in enumerate(result): data = row.items() if is_dict_obj else enumerate(row) for key, value in data: - if not key in convertible_columns or not conversion_factors[row_idx]: + if key not in convertible_columns or not conversion_factors[row_idx-1]: continue if convertible_columns.get(key) == 'rate': - new_value = flt(value) * conversion_factors[row_idx] + new_value = flt(value) * conversion_factors[row_idx-1] else: - new_value = flt(value) / conversion_factors[row_idx] + new_value = flt(value) / conversion_factors[row_idx-1] if not is_dict_obj: row.insert(key+1, new_value) From 23b7591c913e0cdab590ca0277480f6c2811c31c Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 18:23:00 +0530 Subject: [PATCH 099/229] fix: SO to PO Mapping Issue - removed type: GET , which made the URL longer - Added only relevant fields from Items table to Dialog table to reduce args load - Made separate method to set table data in dialog - Added freeze message --- .../doctype/sales_order/sales_order.js | 42 ++++++++++--------- .../doctype/sales_order/sales_order.py | 8 +++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 989bd33e42..203b840db9 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -572,12 +572,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( "fieldname": "against_default_supplier", "default": 0 }, - { - "fieldtype": "Section Break", - "label": "", - "fieldname": "sec_break_dialog", - "hide_border": 1 - }, { fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items', fields: [ @@ -616,16 +610,13 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( read_only:1, in_list_view:1 }, - ], - data: me.frm.doc.items.map((item) =>{ - item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); - return item; - }).filter((item) => {return item.pending_qty > 0;}) + ] } ], primary_action_label: 'Create Purchase Order', primary_action (args) { if (!args) return; + let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children(); if(selected_items.length == 0) { frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'}) @@ -635,8 +626,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order" return frappe.call({ - type: "GET", method: "erpnext.selling.doctype.sales_order.sales_order." + method, + freeze: true, + freeze_message: __("Creating Purchase Order ..."), args: { "source_name": me.frm.doc.name, "selected_items": selected_items @@ -660,8 +652,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } }); - dialog.fields_dict["against_default_supplier"].df.onchange = () => { - console.log("yo"); + dialog.fields_dict["against_default_supplier"].df.onchange = () => set_po_items_data(dialog); + + function set_po_items_data (dialog) { var against_default_supplier = dialog.get_value("against_default_supplier"); var items_for_po = dialog.get_value("items_for_po"); @@ -671,16 +664,27 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( dialog.fields_dict["items_for_po"].df.data = items_with_supplier; dialog.get_field("items_for_po").refresh(); } else { - let pending_items = me.frm.doc.items.map((item) =>{ - item.pending_qty = (flt(item.stock_qty) - flt(item.ordered_qty)) / flt(item.conversion_factor); - return item; - }).filter((item) => {return item.pending_qty > 0;}); + let po_items = []; + me.frm.doc.items.forEach(d => { + let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + if (pending_qty > 0) { + po_items.push({ + "name": d.name, + "item_name": d.item_name, + "item_code": d.item_code, + "pending_qty": pending_qty, + "uom": d.uom, + "supplier": d.supplier + }); + } + }); - dialog.fields_dict["items_for_po"].df.data = pending_items; + dialog.fields_dict["items_for_po"].df.data = po_items; dialog.get_field("items_for_po").refresh(); } } + set_po_items_data(dialog); dialog.get_field("items_for_po").grid.only_sortable(); dialog.get_field("items_for_po").refresh(); dialog.wrapper.find('.grid-heading-row .grid-row-check').click(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ae227e0110..ec1c82339b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -779,7 +779,9 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_purchase_order_for_default_supplier(source_name, selected_items=[], target_doc=None): +def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): + if not selected_items: return + if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -878,7 +880,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() -def make_purchase_order(source_name, selected_items=[], target_doc=None): +def make_purchase_order(source_name, selected_items=None, target_doc=None): + if not selected_items: return + if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) From cdc17bb9f3c6b2f1eab818ed04ed587da58013b0 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 4 Nov 2020 19:28:55 +0530 Subject: [PATCH 100/229] fix: added code for testing --- .../doctype/sales_order/test_sales_order.py | 1 + .../delivery_note/test_delivery_note.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a33d401b57..643e7cf38b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1064,6 +1064,7 @@ def make_sales_order(**args): so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" + so.po_no = args.po_no or '12345' if args.selling_price_list: so.selling_price_list = args.selling_price_list diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0168613415..9566af7b38 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -442,9 +442,15 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(dn.status, "To Bill") self.assertEqual(dn.per_billed, 0) + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, so.po_no) + si = make_sales_invoice(dn.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + dn.load_from_db() self.assertEqual(dn.get("items")[0].billed_amt, 200) self.assertEqual(dn.per_billed, 100) @@ -461,16 +467,25 @@ class TestDeliveryNote(unittest.TestCase): si.insert() si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) dn1 = make_delivery_note(so.name) dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn1.po_no) + dn2 = make_delivery_note(so.name) dn2.get("items")[0].qty = 3 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn2.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) @@ -492,9 +507,15 @@ class TestDeliveryNote(unittest.TestCase): dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, so.po_no) + si1 = make_sales_invoice(dn1.name) si1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, si1.po_no) + dn1.load_from_db() self.assertEqual(dn1.per_billed, 100) @@ -502,10 +523,16 @@ class TestDeliveryNote(unittest.TestCase): si2.get("items")[0].qty = 4 si2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(si2.po_no, so.po_no) + dn2 = make_delivery_note(so.name) dn2.get("items")[0].qty = 5 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn2.po_no, so.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) @@ -525,9 +552,15 @@ class TestDeliveryNote(unittest.TestCase): si = make_sales_invoice(so.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + dn = make_delivery_note(si.name) dn.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + self.assertEqual(dn.get("items")[0].billed_amt, 1000) self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.status, "Completed") From aa08fb971659ffd3e801db2954d06277238a6c25 Mon Sep 17 00:00:00 2001 From: igormbq Date: Wed, 4 Nov 2020 11:40:57 -0300 Subject: [PATCH 101/229] Add location on Asset to use make_demo --- erpnext/demo/data/asset.json | 21 ++++++++++++++------- erpnext/demo/data/location.json | 22 ++++++++++++++++++++++ erpnext/demo/setup/manufacture.py | 1 + erpnext/demo/user/stock.py | 2 +- 4 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 erpnext/demo/data/location.json diff --git a/erpnext/demo/data/asset.json b/erpnext/demo/data/asset.json index 23029ca5e3..44db2ae9e1 100644 --- a/erpnext/demo/data/asset.json +++ b/erpnext/demo/data/asset.json @@ -4,48 +4,55 @@ "item_code": "Computer", "gross_purchase_amount": 100000, "asset_owner": "Company", - "available_for_use_date": "2017-01-02" + "available_for_use_date": "2017-01-02", + "location": "Main Location" }, { "asset_name": "Macbook Air - 1", "item_code": "Computer", "gross_purchase_amount": 60000, "asset_owner": "Company", - "available_for_use_date": "2017-10-02" + "available_for_use_date": "2017-10-02", + "location": "Avg Location" }, { "asset_name": "Conferrence Table", "item_code": "Table", "gross_purchase_amount": 30000, "asset_owner": "Company", - "available_for_use_date": "2018-10-02" + "available_for_use_date": "2018-10-02", + "location": "Zany Location" }, { "asset_name": "Lunch Table", "item_code": "Table", "gross_purchase_amount": 20000, "asset_owner": "Company", - "available_for_use_date": "2018-06-02" + "available_for_use_date": "2018-06-02", + "location": "Fletcher Location" }, { "asset_name": "ERPNext", "item_code": "ERP", "gross_purchase_amount": 100000, "asset_owner": "Company", - "available_for_use_date": "2018-09-02" + "available_for_use_date": "2018-09-02", + "location":"Main Location" }, { "asset_name": "Chair 1", "item_code": "Chair", "gross_purchase_amount": 10000, "asset_owner": "Company", - "available_for_use_date": "2018-07-02" + "available_for_use_date": "2018-07-02", + "location": "Zany Location" }, { "asset_name": "Chair 2", "item_code": "Chair", "gross_purchase_amount": 10000, "asset_owner": "Company", - "available_for_use_date": "2018-07-02" + "available_for_use_date": "2018-07-02", + "location": "Avg Location" } ] diff --git a/erpnext/demo/data/location.json b/erpnext/demo/data/location.json new file mode 100644 index 0000000000..b521aa08c4 --- /dev/null +++ b/erpnext/demo/data/location.json @@ -0,0 +1,22 @@ +[ + { + "location_name": "Main Location", + "latitude": 40.0, + "longitude": 20.0 + }, + { + "location_name": "Avg Location", + "latitude": 63.0, + "longitude": 99.3 + }, + { + "location_name": "Zany Location", + "latitude": 47.5, + "longitude": 10.0 + }, + { + "location_name": "Fletcher Location", + "latitude": 100.90, + "longitude": 80 + } +] \ No newline at end of file diff --git a/erpnext/demo/setup/manufacture.py b/erpnext/demo/setup/manufacture.py index d3846369cd..7d6b5012ea 100644 --- a/erpnext/demo/setup/manufacture.py +++ b/erpnext/demo/setup/manufacture.py @@ -9,6 +9,7 @@ from erpnext.demo.domains import data from six import iteritems def setup_data(): + import_json("Location") import_json("Asset Category") setup_item() setup_workstation() diff --git a/erpnext/demo/user/stock.py b/erpnext/demo/user/stock.py index f95a6b8331..d44da7d127 100644 --- a/erpnext/demo/user/stock.py +++ b/erpnext/demo/user/stock.py @@ -79,7 +79,7 @@ def make_stock_reconciliation(): if item.qty: item.qty = item.qty - round(random.randint(1, item.qty)) try: - stock_reco.insert(ignore_permissions=True) + stock_reco.insert(ignore_permissions=True, ignore_mandatory=True) stock_reco.submit() frappe.db.commit() except OpeningEntryAccountError: From 2b8aa91cf2a986e2a122e01aac8357075aa50aff Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 21:00:52 +0530 Subject: [PATCH 102/229] fix: Make sure row object has doctype so that read only property is applied to rows --- erpnext/selling/doctype/sales_order/sales_order.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 203b840db9..f162291b7c 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -669,6 +669,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ + "doctype": "Sales Order Item", "name": d.name, "item_name": d.item_name, "item_code": d.item_code, From 5de719fc747c2cad037c12d12a4c34b54987d5aa Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 4 Nov 2020 21:22:22 +0530 Subject: [PATCH 103/229] chore: Delete Quoted Item Comparison Report --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5ef53715fb..678c7e148f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -733,3 +733,4 @@ erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee +execute:frappe.delete_doc("Report", "Quoted Item Comparison") \ No newline at end of file From 421348a5f2c674cb977dcf96ceab42ada2386733 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 5 Nov 2020 15:35:48 +0530 Subject: [PATCH 104/229] fix: not able to select assign to --- erpnext/assets/doctype/asset_maintenance/asset_maintenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 60c528bcc4..a506deec93 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -108,7 +108,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_team_members(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }) + return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }, "team_member") @frappe.whitelist() def get_maintenance_log(asset_name): From 5c93ca7bef93ae5445c2f018afeae82cab915b48 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 5 Nov 2020 15:45:15 +0530 Subject: [PATCH 105/229] fix: leave ledger patch (#23807) Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> --- erpnext/patches.txt | 2 +- erpnext/patches/v12_0/generate_leave_ledger_entries.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 678c7e148f..34dbdd0bd5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 +erpnext.patches.v12_0.generate_leave_ledger_entries #04-11-2020 execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 342c12996d..7afde373c3 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -11,8 +11,6 @@ def execute(): frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") frappe.reload_doc("HR", "doctype", "Leave Encashment") frappe.reload_doc("HR", "doctype", "Leave Type") - if frappe.db.a_row_exists("Leave Ledger Entry"): - return if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"): frappe.reload_doc("HR", "doctype", "Leave Allocation") From 403822afb315b80c1c48c05ccf9749ba08ff73ae Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 5 Nov 2020 16:24:33 +0530 Subject: [PATCH 106/229] fix: correcting description field in taxes and charges for accounts that have account number + account name --- erpnext/public/js/controllers/accounts.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 6e97d811fc..b1ffcfb6ad 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -146,18 +146,21 @@ cur_frm.cscript.account_head = function(doc, cdt, cdn) { if(!d.charge_type && d.account_head){ frappe.msgprint(__("Please select Charge Type first")); frappe.model.set_value(cdt, cdn, "account_head", ""); - } else if(d.account_head && d.charge_type!=="Actual") { + } else if(d.account_head) { frappe.call({ type:"GET", method: "erpnext.controllers.accounts_controller.get_tax_rate", args: {"account_head":d.account_head}, callback: function(r) { - frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0); - frappe.model.set_value(cdt, cdn, "description", r.message.account_name); + if(d.charge_type!=="Actual"){ + frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0); + frappe.model.set_value(cdt, cdn, "description", r.message.account_name); + } + else if(d.charge_type == 'Actual'){ + frappe.model.set_value(cdt, cdn, "description", r.message.account_name); + } } }) - } else if (d.charge_type == 'Actual' && d.account_head) { - frappe.model.set_value(cdt, cdn, "description", d.account_head.split(' - ')[0]); } } From 59ccb64e18422f34e9b34f9149913d2db52437fc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 16:29:34 +0530 Subject: [PATCH 107/229] fix: Auto Statewise gst tax template --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 69e47a43c4..bc8cb11a95 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -236,7 +236,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): 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') + {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') return default_tax def get_tax_template_for_sez(party_details, master_doctype, company, party_type): From 19df4534f50751f91577f8ec3fab56fad5892360 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 5 Nov 2020 16:37:40 +0530 Subject: [PATCH 108/229] fix: asset finance book depreciation posting date fix (#23833) --- .../doctype/asset_finance_book/asset_finance_book.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index d422876047..d9b7b695f7 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -50,13 +50,11 @@ "reqd": 1 }, { - "depends_on": "eval:parent.doctype == 'Asset'", "fieldname": "depreciation_start_date", "fieldtype": "Date", "in_list_view": 1, "label": "Depreciation Posting Date", - "mandatory_depends_on": "eval:parent.doctype == 'Asset'", - "reqd": 1 + "mandatory_depends_on": "eval:parent.doctype == 'Asset'" }, { "default": "0", @@ -87,7 +85,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-30 15:22:29.119868", + "modified": "2020-11-05 16:30:09.213479", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", From bc919e2e7db2571ade74594473835b01c6a7a6b9 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 5 Nov 2020 17:38:35 +0530 Subject: [PATCH 109/229] fix: cannot expand row in update items dialog (#23839) --- erpnext/public/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index ea2093eee1..891bbe5b59 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -539,7 +539,7 @@ erpnext.utils.update_child_items = function(opts) { fieldtype: "Table", label: "Items", cannot_add_rows: cannot_add_row, - in_place_edit: true, + in_place_edit: false, reqd: 1, data: this.data, get_data: () => { From 5b02d328269e2654f6253c45ddafe215121d2435 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 5 Nov 2020 18:04:14 +0530 Subject: [PATCH 110/229] fix: trailling spaces removed --- erpnext/public/js/controllers/accounts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index b1ffcfb6ad..1bceec0547 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -155,7 +155,7 @@ cur_frm.cscript.account_head = function(doc, cdt, cdn) { if(d.charge_type!=="Actual"){ frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0); frappe.model.set_value(cdt, cdn, "description", r.message.account_name); - } + } else if(d.charge_type == 'Actual'){ frappe.model.set_value(cdt, cdn, "description", r.message.account_name); } From 3a9dc90695033828a6d24d61eb6b1106fc9f9689 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Thu, 5 Nov 2020 18:51:56 +0530 Subject: [PATCH 111/229] fix: fix: replaced formatdate -> format_date (#23829) --- .../journal_auditing_voucher/journal_auditing_voucher.html | 2 +- .../payment_receipt_voucher/payment_receipt_voucher.html | 2 +- .../purchase_auditing_voucher/purchase_auditing_voucher.html | 4 ++-- .../sales_auditing_voucher/sales_auditing_voucher.html | 4 ++-- erpnext/templates/pages/material_request_info.html | 2 +- erpnext/templates/pages/order.html | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html index 1351517981..0ca940f8bd 100644 --- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html +++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html @@ -17,7 +17,7 @@
{%= report_columns[0].label %}{%= report_columns[1].label %}
- +
Date: {{ frappe.utils.formatdate(doc.creation) }}
Date: {{ frappe.utils.format_date(doc.creation) }}
diff --git a/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html b/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html index f2e65d3334..283d505e3b 100644 --- a/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html +++ b/erpnext/accounts/print_format/payment_receipt_voucher/payment_receipt_voucher.html @@ -5,7 +5,7 @@ {{ add_header(0, 1, doc, letter_head, no_letterhead, print_settings) }} {%- for label, value in ( - (_("Received On"), frappe.utils.formatdate(doc.voucher_date)), + (_("Received On"), frappe.utils.format_date(doc.voucher_date)), (_("Received From"), doc.pay_to_recd_from), (_("Amount"), "" + doc.get_formatted("total_amount") + "
" + (doc.total_amount_in_words or "") + "
"), (_("Remarks"), doc.remark) diff --git a/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html b/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html index a7c3bce0b4..043ac254ed 100644 --- a/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html +++ b/erpnext/accounts/print_format/purchase_auditing_voucher/purchase_auditing_voucher.html @@ -8,7 +8,7 @@
- + @@ -17,7 +17,7 @@
Supplier Name: {{ doc.supplier }}
Due Date: {{ frappe.utils.formatdate(doc.due_date) }}
Due Date: {{ frappe.utils.format_date(doc.due_date) }}
Address: {{doc.address_display}}
Contact: {{doc.contact_display}}
Mobile no: {{doc.contact_mobile}}
- +
Voucher No: {{ doc.name }}
Date: {{ frappe.utils.formatdate(doc.creation) }}
Date: {{ frappe.utils.format_date(doc.creation) }}
diff --git a/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html b/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html index ef4ada14a3..a53b593a72 100644 --- a/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html +++ b/erpnext/accounts/print_format/sales_auditing_voucher/sales_auditing_voucher.html @@ -8,7 +8,7 @@
- + @@ -17,7 +17,7 @@
Customer Name: {{ doc.customer }}
Due Date: {{ frappe.utils.formatdate(doc.due_date) }}
Due Date: {{ frappe.utils.format_date(doc.due_date) }}
Address: {{doc.address_display}}
Contact: {{doc.contact_display}}
Mobile no: {{doc.contact_mobile}}
- +
Voucher No: {{ doc.name }}
Date: {{ frappe.utils.formatdate(doc.creation) }}
Date: {{ frappe.utils.format_date(doc.creation) }}
diff --git a/erpnext/templates/pages/material_request_info.html b/erpnext/templates/pages/material_request_info.html index 9d189895b6..c7a78027b1 100644 --- a/erpnext/templates/pages/material_request_info.html +++ b/erpnext/templates/pages/material_request_info.html @@ -25,7 +25,7 @@
- {{ frappe.utils.formatdate(doc.transaction_date, 'medium') }} + {{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 01b5f6df6c..af7af11677 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -42,10 +42,10 @@
- {{ frappe.utils.formatdate(doc.transaction_date, 'medium') }} + {{ frappe.utils.format_date(doc.transaction_date, 'medium') }} {% if doc.valid_till %}

- {{ _("Valid Till") }}: {{ frappe.utils.formatdate(doc.valid_till, 'medium') }} + {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}

{% endif %}
From 49cc57e76eb61bec1d05c2e6b4f7f15b12008204 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 21:14:07 +0530 Subject: [PATCH 112/229] fix: Negative amount check for amounts --- erpnext/loan_management/doctype/loan/loan.json | 3 ++- .../doctype/loan_disbursement/loan_disbursement.json | 3 ++- .../doctype/loan_repayment/loan_repayment.json | 3 ++- erpnext/loan_management/doctype/pledge/pledge.json | 7 +++++-- .../doctype/proposed_pledge/proposed_pledge.json | 8 ++++++-- erpnext/loan_management/doctype/unpledge/unpledge.json | 4 +++- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 23996afd21..e8ecf015c3 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -136,6 +136,7 @@ "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", + "non_negative": 1, "options": "Company:company:default_currency" }, { @@ -351,7 +352,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-27 23:37:02.785940", + "modified": "2020-11-05 10:04:00.762975", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index 89f671bcc0..cd5df4d3cd 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -41,6 +41,7 @@ "fieldname": "disbursed_amount", "fieldtype": "Currency", "label": "Disbursed Amount", + "non_negative": 1, "options": "Company:company:default_currency", "reqd": 1 }, @@ -130,7 +131,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-16 10:04:26.229216", + "modified": "2020-11-06 10:04:30.882322", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 60b20369dc..2b5df4be24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -108,6 +108,7 @@ "fieldname": "amount_paid", "fieldtype": "Currency", "label": "Amount Paid", + "non_negative": 1, "options": "Company:company:default_currency", "reqd": 1 }, @@ -230,7 +231,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-10 03:49:01.827593", + "modified": "2020-11-05 10:06:58.792841", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json index f22a21e3be..801e3a3117 100644 --- a/erpnext/loan_management/doctype/pledge/pledge.json +++ b/erpnext/loan_management/doctype/pledge/pledge.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-09-09 17:06:16.756573", "doctype": "DocType", "editable_grid": 1, @@ -49,7 +50,8 @@ "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Quantity" + "label": "Quantity", + "non_negative": 1 }, { "fieldname": "loan_security_price", @@ -86,7 +88,8 @@ } ], "istable": 1, - "modified": "2019-12-03 10:59:58.001421", + "links": [], + "modified": "2020-11-05 10:07:15.424937", "modified_by": "Administrator", "module": "Loan Management", "name": "Pledge", diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json index aee7c2ced5..3e7e778a25 100644 --- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-29 22:29:37.628178", "doctype": "DocType", "editable_grid": 1, @@ -39,7 +40,8 @@ "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Quantity" + "label": "Quantity", + "non_negative": 1 }, { "fieldname": "loan_security", @@ -56,8 +58,10 @@ "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-12-02 10:23:11.498308", + "links": [], + "modified": "2020-11-05 10:07:37.542344", "modified_by": "Administrator", "module": "Loan Management", "name": "Proposed Pledge", diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json index ee192d7377..00356685eb 100644 --- a/erpnext/loan_management/doctype/unpledge/unpledge.json +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -52,6 +52,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "reqd": 1 }, { @@ -62,9 +63,10 @@ "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-05-06 10:50:18.448552", + "modified": "2020-11-05 10:07:28.106961", "modified_by": "Administrator", "module": "Loan Management", "name": "Unpledge", From a2bff7fbfcbdcda76143dab00468655232105fd7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 21:14:29 +0530 Subject: [PATCH 113/229] fix: Penalty amount calculation fix --- .../loan_management/doctype/loan/test_loan.py | 60 +++++++++---------- .../doctype/loan_repayment/loan_repayment.py | 4 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 1634697939..10a7b1143d 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -142,19 +142,19 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days) + / (days_in_year(get_datetime(first_date).year) * 100), 2) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111118.68) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119) repayment_entry.save() repayment_entry.submit() - penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) - self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) + penalty_amount = (accrued_interest_amount * 5 * 25) / 100 + self.assertEquals(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0)) amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount']) @@ -162,8 +162,8 @@ class TestLoan(unittest.TestCase): total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount'] self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable) - self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid - - penalty_amount - total_interest_paid, 2)) + self.assertEquals(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid - + penalty_amount - total_interest_paid, 0)) def test_loan_closure(self): pledge = [{ @@ -184,10 +184,10 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - # Adding 6 since repayment is made 5 days late after due date + # Adding 5 since repayment is made 5 days late after due date # and since payment type is loan closure so interest should be considered for those - # 6 days as well though in grace period - no_of_days += 6 + # 5 days as well though in grace period + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -195,7 +195,7 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() @@ -292,7 +292,7 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -300,7 +300,7 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() request_loan_closure(loan.name) @@ -318,7 +318,7 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) - amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertTrue(amounts['pending_principal_amount'] < 0) self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) @@ -392,7 +392,7 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -400,9 +400,9 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', @@ -412,7 +412,7 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertTrue(amounts['pending_principal_amount'] < 0.0) def test_partial_unaccrued_interest_payment(self): @@ -443,9 +443,9 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), paid_amount) repayment_entry.submit() @@ -480,15 +480,15 @@ class TestLoan(unittest.TestCase): amounts = calculate_amounts(loan.name, add_days(last_date, 1)) paid_amount = amounts['interest_amount']/2 - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), paid_amount) repayment_entry.submit() # 30 days - grace period - penalty_days = 30 - 5 + penalty_days = 30 - 4 penalty_applicable_amount = flt(amounts['interest_amount']/2, 2) - penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days)/365, 2) + penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2) process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', @@ -514,7 +514,7 @@ class TestLoan(unittest.TestCase): last_date = '2019-10-30' no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -523,7 +523,7 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) # repay 50 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount - 50)) repayment_entry.submit() @@ -533,7 +533,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50) request_loan_closure(loan.name) @@ -558,7 +558,7 @@ class TestLoan(unittest.TestCase): last_date = '2019-10-30' no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -567,7 +567,7 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) # repay 100 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount - 100)) repayment_entry.submit() @@ -577,13 +577,13 @@ class TestLoan(unittest.TestCase): self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100) we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount']) we.submit() - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 63d388daf5..7216c8b813 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -341,10 +341,10 @@ def get_amounts(amounts, against_loan, posting_date): due_date = add_days(entry.posting_date, 1) no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + add_days(due_date, loan_type_details.grace_period_in_days)) + 1 if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': - penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 + penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) total_pending_interest += entry.interest_amount payable_principal_amount += entry.payable_principal_amount From 1145b3796c50308c529955a9654c3afdd27e6470 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 21:21:40 +0530 Subject: [PATCH 114/229] fix: Remove accrual type from process --- .../loan_interest_accrual/loan_interest_accrual.py | 5 +++-- .../process_loan_interest_accrual.json | 9 +-------- .../process_loan_interest_accrual.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index d0b957de56..70d1453df4 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -135,7 +135,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte for loan in open_loans: calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type) -def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None): +def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"): curr_date = posting_date or add_days(nowdate(), 1) term_loans = get_term_loans(curr_date, term_loan, loan_type) @@ -154,7 +154,8 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere 'payable_principal': loan.principal_amount, 'process_loan_interest': process_loan_interest, 'repayment_schedule_name': loan.payment_entry, - 'posting_date': posting_date + 'posting_date': posting_date, + 'accrual_type': accrual_type }) make_loan_interest_accrual_entry(args) diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index c1296f759f..4c354e6721 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -10,7 +10,6 @@ "loan_type", "loan", "process_type", - "accrual_type", "amended_from" ], "fields": [ @@ -48,18 +47,12 @@ "hidden": 1, "label": "Process Type", "read_only": 1 - }, - { - "fieldname": "accrual_type", - "fieldtype": "Select", - "label": "Accrual Type", - "options": "Regular\nRepayment\nDisbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-26 07:14:31.491249", + "modified": "2020-11-05 10:49:35.657728", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 1eeb18b7ea..f0522657eb 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -20,7 +20,7 @@ class ProcessLoanInterestAccrual(Document): if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) + open_loans = open_loans, loan_type = self.loan_type) if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, From 815341d45457e7c6ea7bde896d3f7b20b82d37eb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 5 Nov 2020 16:57:23 +0100 Subject: [PATCH 115/229] feat: validate fiscal year --- .../regional/germany/utils/datev/datev_csv.py | 2 +- erpnext/regional/report/datev/datev.py | 26 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index aae734f8e2..fb7ca71e79 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -104,7 +104,7 @@ def get_header(filters, csv_class): # L = Tax client number (Mandantennummer) datev_settings.get('client_number', '00000'), # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(frappe.defaults.get_user_default('year_start_date'), 'yyyyMMdd'), + frappe.utils.formatdate(filters.get('fiscal_year_start'), 'yyyyMMdd'), # N = Length of account numbers (Sachkontenlänge) datev_settings.get('account_number_length', '4'), # O = Transaction batch start date (YYYYMMDD) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index dd818e6054..04f8928c6a 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -13,7 +13,7 @@ import json import frappe from frappe import _ from six import string_types -from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv +from erpnext.accounts.utils import get_fiscal_year from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames COLUMNS = [ @@ -98,21 +98,33 @@ def execute(filters=None): def validate(filters): """Make sure all mandatory filters and settings are present.""" - if not filters.get('company'): + company = filters.get('company') + if not company: frappe.throw(_('Company is a mandatory filter.')) - if not filters.get('from_date'): + from_date = filters.get('from_date') + if not from_date: frappe.throw(_('From Date is a mandatory filter.')) - if not filters.get('to_date'): + to_date = filters.get('to_date') + if not to_date: frappe.throw(_('To Date is a mandatory filter.')) + validate_fiscal_year(from_date, to_date, company) + try: frappe.get_doc('DATEV Settings', filters.get('company')) except frappe.DoesNotExistError: frappe.throw(_('Please create DATEV Settings for Company {}.').format(filters.get('company'))) +def validate_fiscal_year(from_date, to_date, company): + from_fiscal_year = get_fiscal_year(date=from_date, company=company) + to_fiscal_year = get_fiscal_year(date=to_date, company=company) + if from_fiscal_year != to_fiscal_year: + frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date)) + + def get_transactions(filters, as_dict=1): """ Get a list of accounting entries. @@ -317,9 +329,13 @@ def download_datev_csv(filters): filters = json.loads(filters) validate(filters) + company = filters.get('company') + + fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=company) + filters['fiscal_year_start'] = fiscal_year[1] # set chart of accounts used - coa = frappe.get_value('Company', filters.get('company'), 'chart_of_accounts') + coa = frappe.get_value('Company', company, 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') transactions = get_transactions(filters) From f91d82b5389972d0f952115cb41a30089a054c6b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 5 Nov 2020 17:28:39 +0100 Subject: [PATCH 116/229] fix: imports --- erpnext/regional/report/datev/datev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 04f8928c6a..738806321c 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -14,6 +14,7 @@ import frappe from frappe import _ from six import string_types from erpnext.accounts.utils import get_fiscal_year +from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames COLUMNS = [ From c3307f79004f534fa95be13dd3de5c96dbce8089 Mon Sep 17 00:00:00 2001 From: Anupam Date: Fri, 6 Nov 2020 12:28:38 +0530 Subject: [PATCH 117/229] fix: replaced formatdate -> format_date --- .../bank_and_cash_payment_voucher.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html b/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html index 6fe6999051..e588ed6609 100644 --- a/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html +++ b/erpnext/accounts/print_format/bank_and_cash_payment_voucher/bank_and_cash_payment_voucher.html @@ -19,7 +19,7 @@
- +
Date: {{ frappe.utils.formatdate(doc.creation) }}
Date: {{ frappe.utils.format_date(doc.creation) }}
From 06ba92be7a2f53e39e0adeecbbfd006388c554cb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Nov 2020 12:57:25 +0530 Subject: [PATCH 118/229] fix: Added link of bank reconciliation and clearance in accounting desk page --- erpnext/accounts/desk_page/accounting/accounting.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index b0371e7c09..1d9236b81e 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-10-08 20:31:46.022470", + "modified": "2020-11-04 13:05:37.922171", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -108,7 +108,7 @@ "pin_to_top": 0, "shortcuts": [ { - "label": "Chart of Accounts", + "label": "Chart Of Accounts", "link_to": "Account", "type": "DocType" }, From 0b4946ba2799750eae91174d755d36dbffe68bb4 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 6 Nov 2020 13:08:53 +0530 Subject: [PATCH 119/229] fix: Add missing links - Added Bank Transaction Statement Entry and Bank Statement Settings --- erpnext/accounts/desk_page/accounting/accounting.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 1d9236b81e..9172792411 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-11-04 13:05:37.922171", + "modified": "2020-11-06 13:05:58.650150", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", From 3e69756e154a5b3cc1ca467f1c8d9bf7e77a2e60 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 6 Nov 2020 14:25:09 +0530 Subject: [PATCH 120/229] fix: Add better remarks for Loan GL entries --- .../doctype/loan_disbursement/loan_disbursement.py | 4 ++-- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 6 ++++-- .../doctype/loan_repayment/loan_repayment.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 949e1412e2..bda439fb7c 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -123,7 +123,7 @@ class LoanDisbursement(AccountsController): "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": "Against Loan:" + self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -139,7 +139,7 @@ class LoanDisbursement(AccountsController): "credit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": "Against Loan:" + self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 70d1453df4..22ff6663d3 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -49,7 +49,8 @@ class LoanInterestAccrual(AccountsController): "debit_in_account_currency": self.interest_amount, "against_voucher_type": "Loan", "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, + "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( + get_last_accural_date(self.loan), self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) @@ -65,7 +66,8 @@ class LoanInterestAccrual(AccountsController): "credit_in_account_currency": self.interest_amount, "against_voucher_type": "Loan", "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, + "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( + get_last_accural_date(self.loan), self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 7216c8b813..e478cb8d43 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -217,7 +217,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -233,7 +233,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, From 3899079bb9b0ef733e5b1100359afd7d94cfba5a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 6 Nov 2020 17:39:54 +0530 Subject: [PATCH 121/229] fix: Loan seurity unpledge msg improvement --- .../loan_security_shortfall.py | 2 +- .../loan_security_unpledge/loan_security_unpledge.py | 12 ++++++++++-- .../process_loan_interest_accrual.json | 9 ++++++++- .../process_loan_interest_accrual.py | 4 ++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 0f42bde3c4..8ec0bfb62c 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -22,7 +22,7 @@ def update_shortfall_status(loan, security_value): if security_value >= loan_security_shortfall.shortfall_amount: frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, { "status": "Completed", - "shortfall_value": loan_security_shortfall.shortfall_amount}) + "shortfall_amount": loan_security_shortfall.shortfall_amount}) else: frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 5d4447bf2b..c29f325bfc 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -67,10 +67,18 @@ class LoanSecurityUnpledge(Document): security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: - frappe.throw("Cannot Unpledge, loan to value ratio is breaching") + self._throw(security_value, pending_principal_amount, ltv_ratio) if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: - frappe.throw("Cannot Unpledge, loan to value ratio is breaching") + self._throw(security_value, pending_principal_amount, ltv_ratio) + + def _throw(self, security_value, pending_principal_amount, ltv_ratio): + msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value)) + msg += '
' + msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2))) + msg += '
' + msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio)) + frappe.throw(msg, title=_("Loan To Value ratio breach")) def on_update_after_submit(self): self.approve() diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 4c354e6721..b78c3ba5d8 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -10,6 +10,7 @@ "loan_type", "loan", "process_type", + "accrual_type", "amended_from" ], "fields": [ @@ -47,12 +48,18 @@ "hidden": 1, "label": "Process Type", "read_only": 1 + }, + { + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nDisbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-05 10:49:35.657728", + "modified": "2020-11-06 04:43:56.581670", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index f0522657eb..11333dc2aa 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -20,11 +20,11 @@ class ProcessLoanInterestAccrual(Document): if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type) + open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, - loan_type=self.loan_type) + loan_type=self.loan_type, accrual_type=self.accrual_type) def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"): From e0af8fe8e5778b285cf800f9439e032fad5198d2 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 6 Nov 2020 18:19:36 +0530 Subject: [PATCH 122/229] fix: auto fetch sr nos with modifed conversion factor (#23854) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 8 ++++++-- erpnext/selling/page/point_of_sale/pos_controller.js | 2 ++ erpnext/selling/page/point_of_sale/pos_item_details.js | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index b0a7547ce8..a7e20a0c32 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -132,15 +132,19 @@ class POSInvoice(SalesInvoice): msg = "" item_code = frappe.bold(d.item_code) + serial_nos = get_serial_nos(d.serial_no) if serialized and batched and (no_batch_selected or no_serial_selected): msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.') .format(d.idx, item_code)) - if serialized and no_serial_selected: + elif serialized and no_serial_selected: msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.') .format(d.idx, item_code)) - if batched and no_batch_selected: + elif batched and no_batch_selected: msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.') .format(d.idx, item_code)) + elif serialized and not no_serial_selected and len(serial_nos) != d.qty: + msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code)) + if msg: error_msg.append(msg) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index f7d1fa4ded..970d840665 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -555,6 +555,8 @@ erpnext.PointOfSale.Controller = class { frappe.utils.play_sound("error"); return; } + if (!item_code) return; + item_selected_from_selector && (value = flt(value)) const args = { item_code, batch_no, [field]: value }; diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 9874d1b5f9..a4de9f165d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -372,12 +372,13 @@ erpnext.PointOfSale.ItemDetails = class { this.$form_container.on('click', '.auto-fetch-btn', () => { this.batch_no_control && this.batch_no_control.set_value(''); let qty = this.qty_control.get_value(); + let conversion_factor = this.conversion_factor_control.get_value(); let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : ""; let numbers = frappe.call({ method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", args: { - qty, + qty: qty * conversion_factor, item_code: this.current_item.item_code, warehouse: this.warehouse_control.get_value() || '', batch_nos: this.current_item.batch_no || '', From 4d99d695a38d40087e2f9051084f1c9b9016f187 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Fri, 6 Nov 2020 23:33:19 +0530 Subject: [PATCH 123/229] feat(uae vat 201): update desk page entry --- erpnext/accounts/desk_page/accounting/accounting.json | 8 ++++---- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 64e2b246a7..de9ed9fca1 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -83,7 +83,7 @@ { "hidden": 0, "label": "Value-Added Tax (VAT UAE)", - "links": "[\n {\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Modules", @@ -103,8 +103,8 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-10-08 20:31:46.022470", - "modified_by": "Administrator", + "modified": "2020-11-06 23:30:39.515679", + "modified_by": "moha@gmail.com", "module": "Accounts", "name": "Accounting", "onboarding": "Accounts", @@ -158,4 +158,4 @@ "type": "Dashboard" } ] -} +} \ No newline at end of file diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index de727d2ecd..b0614238ba 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -30,14 +30,12 @@ def get_columns(): "label": _("Amount (AED)"), "fieldtype": "Currency", "width": 125, - "options": "currency" }, { "fieldname": "vat_amount", "label": _("VAT Amount (AED)"), "fieldtype": "Currency", "width": 150, - "options": "currency" } ] From c5aad7b6e5f8419c4a2201fff60e20f05362897a Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Fri, 6 Nov 2020 23:36:50 +0530 Subject: [PATCH 124/229] feat(UAE VAT 201): reorder desk page listing --- erpnext/accounts/desk_page/accounting/accounting.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index de9ed9fca1..993d2a625b 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -83,7 +83,7 @@ { "hidden": 0, "label": "Value-Added Tax (VAT UAE)", - "links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n\n]" } ], "category": "Modules", @@ -103,7 +103,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-11-06 23:30:39.515679", + "modified": "2020-11-06 23:34:50.325014", "modified_by": "moha@gmail.com", "module": "Accounts", "name": "Accounting", From 2ad015450ed74a558e693b31ab0b3d27eb82752b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 Nov 2020 00:14:40 +0530 Subject: [PATCH 125/229] fix: Remarks fix --- erpnext/loan_management/doctype/loan/loan.js | 3 ++- .../loan_interest_accrual/loan_interest_accrual.json | 10 +++++++++- .../loan_interest_accrual/loan_interest_accrual.py | 11 +++++++---- .../doctype/loan_repayment/loan_repayment.py | 7 +++---- .../process_loan_interest_accrual.json | 6 ++++-- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 8d101b862a..28af3a9c41 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -7,7 +7,8 @@ frappe.ui.form.on('Loan', { setup: function(frm) { frm.make_methods = { 'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') }, - 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') } + 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') }, + 'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') } } }, onload: function (frm) { diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index 893609e0c7..d6bf08ac51 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -27,6 +27,7 @@ "section_break_15", "process_loan_interest_accrual", "repayment_schedule_name", + "last_accrual_date", "amended_from" ], "fields": [ @@ -163,13 +164,20 @@ "fieldtype": "Currency", "label": "Penalty Amount", "options": "Company:company:default_currency" + }, + { + "fieldname": "last_accrual_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Last Accrual Date", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-11 11:17:44.704694", + "modified": "2020-11-06 13:22:40.197916", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 22ff6663d3..d642400cdc 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -22,6 +22,9 @@ class LoanInterestAccrual(AccountsController): if not self.interest_amount and not self.payable_principal_amount: frappe.throw(_("Interest Amount or Principal Amount is mandatory")) + if not self.last_accrual_date: + self.last_accrual_date = get_last_accrual_date(self.loan) + def on_submit(self): self.make_gl_entries() @@ -50,7 +53,7 @@ class LoanInterestAccrual(AccountsController): "against_voucher_type": "Loan", "against_voucher": self.loan, "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - get_last_accural_date(self.loan), self.posting_date, self.loan), + self.last_accrual_date, self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) @@ -67,7 +70,7 @@ class LoanInterestAccrual(AccountsController): "against_voucher_type": "Loan", "against_voucher": self.loan, "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - get_last_accural_date(self.loan), self.posting_date, self.loan), + self.last_accrual_date, self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) @@ -213,13 +216,13 @@ def make_loan_interest_accrual_entry(args): def get_no_of_days_for_interest_accural(loan, posting_date): - last_interest_accrual_date = get_last_accural_date(loan.name) + last_interest_accrual_date = get_last_accrual_date(loan.name) no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 return no_of_days -def get_last_accural_date(loan): +def get_last_accrual_date(loan): last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` WHERE loan = %s and docstatus = 1""", (loan)) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index e478cb8d43..bb91abd628 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -14,7 +14,7 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans -from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accural_date +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accrual_date class LoanRepayment(AccountsController): @@ -78,7 +78,7 @@ class LoanRepayment(AccountsController): if self.total_interest_paid > self.interest_payable: if not self.is_term_loan: # get last loan interest accrual date - last_accrual_date = get_last_accural_date(self.against_loan) + last_accrual_date = get_last_accrual_date(self.against_loan) # get posting date upto which interest has to be accrued per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, @@ -87,7 +87,6 @@ class LoanRepayment(AccountsController): no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, precision)/per_day_interest, 0) - 1 - posting_date = add_days(last_accrual_date, no_of_days) # book excess interest paid @@ -368,7 +367,7 @@ def get_amounts(amounts, against_loan, posting_date): if due_date: pending_days = date_diff(posting_date, due_date) + 1 else: - last_accrual_date = get_last_accural_date(against_loan_doc.name) + last_accrual_date = get_last_accrual_date(against_loan_doc.name) pending_days = date_diff(posting_date, last_accrual_date) + 1 if pending_days > 0: diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index b78c3ba5d8..828df2e35f 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -52,14 +52,16 @@ { "fieldname": "accrual_type", "fieldtype": "Select", + "hidden": 1, "label": "Accrual Type", - "options": "Regular\nRepayment\nDisbursement" + "options": "Regular\nRepayment\nDisbursement", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-06 04:43:56.581670", + "modified": "2020-11-06 13:28:51.478909", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", From dd94587ef807f6d5e9797a8ddf1db9ff4d9a14ff Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 Nov 2020 17:08:30 +0530 Subject: [PATCH 126/229] fix: Loan write off precision issue --- .../loan_interest_accrual.json | 3 ++- .../doctype/loan_write_off/loan_write_off.js | 18 ++++++++++++++++++ .../doctype/loan_write_off/loan_write_off.py | 6 ++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index d6bf08ac51..f157f0df8f 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -142,6 +142,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.is_term_loan", "fieldname": "paid_principal_amount", "fieldtype": "Currency", "label": "Paid Principal Amount", @@ -177,7 +178,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-06 13:22:40.197916", + "modified": "2020-11-07 05:49:25.448875", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js index cc5cd0d3a0..4e3319c208 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js @@ -4,6 +4,12 @@ {% include 'erpnext/loan_management/loan_common.js' %}; frappe.ui.form.on('Loan Write Off', { + loan: function(frm) { + frm.trigger('show_pending_principal_amount'); + }, + onload: function(frm) { + frm.trigger('show_pending_principal_amount'); + }, refresh: function(frm) { frm.set_query('write_off_account', function(){ return { @@ -14,5 +20,17 @@ frappe.ui.form.on('Loan Write Off', { } } }); + }, + show_pending_principal_amount: function(frm) { + if (frm.doc.loan && frm.doc.docstatus === 0) { + frappe.db.get_value('Loan', frm.doc.loan, ['total_payment', 'total_interest_payable', + 'total_principal_paid', 'written_off_amount'], function(values) { + frm.set_df_property('write_off_amount', 'description', + "Pending principal amount is " + cstr(flt(values.total_payment - values.total_interest_payable + - values.total_principal_paid - values.written_off_amount, 2))); + frm.refresh_field('write_off_amount'); + }); + + } } }); diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 823e6a904f..6e402edcd2 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import getdate, flt +from frappe.utils import getdate, flt, cint from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries @@ -19,10 +19,12 @@ class LoanWriteOff(AccountsController): self.cost_center = erpnext.get_default_cost_center(self.company) def validate_write_off_amount(self): + precision = cint(frappe.db.get_default("currency_precision")) or 2 total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount']) - pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) + pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), + precision) if self.write_off_amount > pending_principal_amount: frappe.throw(_("Write off amount cannot be greater than pending principal amount")) From d53abf194e900aa36d96d9793a889923fbd1901c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 9 Nov 2020 18:34:38 +0530 Subject: [PATCH 127/229] fix: Party for loan ledger entries --- .../loan_management/doctype/loan/loan_list.js | 16 ++++++++++++++++ .../loan_disbursement/loan_disbursement.py | 2 -- .../loan_interest_accrual.py | 2 -- .../doctype/loan_repayment/loan_repayment.py | 4 ---- .../doctype/loan_write_off/loan_write_off.py | 2 -- 5 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 erpnext/loan_management/doctype/loan/loan_list.js diff --git a/erpnext/loan_management/doctype/loan/loan_list.js b/erpnext/loan_management/doctype/loan/loan_list.js new file mode 100644 index 0000000000..6591b72996 --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan_list.js @@ -0,0 +1,16 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.listview_settings['Loan'] = { + get_indicator: function(doc) { + var status_color = { + "Draft": "red", + "Sanctioned": "blue", + "Disbursed": "orange", + "Partially Disbursed": "yellow", + "Loan Closure Requested": "green", + "Closed": "green" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + }, +}; diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index bda439fb7c..233862bcfe 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -141,8 +141,6 @@ class LoanDisbursement(AccountsController): "against_voucher": self.against_loan, "remarks": _("Disbursement against loan:") + self.against_loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": self.disbursement_date }) ) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index d642400cdc..d17f5af490 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -62,8 +62,6 @@ class LoanInterestAccrual(AccountsController): gle_map.append( self.get_gl_dict({ "account": self.interest_income_account, - "party_type": self.applicant_type, - "party": self.applicant, "against": self.loan_account, "credit": self.interest_amount, "credit_in_account_currency": self.interest_amount, diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index bb91abd628..a8887d7b24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -234,8 +234,6 @@ class LoanRepayment(AccountsController): "against_voucher": self.against_loan, "remarks": _("Repayment against loan:") + self.against_loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) @@ -251,8 +249,6 @@ class LoanRepayment(AccountsController): "against_voucher": self.against_loan, "remarks": _("Against Loan:") + self.against_loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 6e402edcd2..54a3f2cbb1 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -63,8 +63,6 @@ class LoanWriteOff(AccountsController): "against_voucher": self.loan, "remarks": _("Against Loan:") + self.loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) From 7915a3acaeb00ffe38a7aeffaa83d4d283b7ae36 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 9 Nov 2020 18:37:28 +0530 Subject: [PATCH 128/229] chore: Update translations (#23856) Co-authored-by: frappe --- erpnext/translations/af.csv | 8 +++-- erpnext/translations/am.csv | 8 +++-- erpnext/translations/ar.csv | 8 +++-- erpnext/translations/bg.csv | 8 +++-- erpnext/translations/bn.csv | 8 +++-- erpnext/translations/bs.csv | 8 +++-- erpnext/translations/ca.csv | 8 +++-- erpnext/translations/cs.csv | 8 +++-- erpnext/translations/da.csv | 8 +++-- erpnext/translations/de.csv | 8 +++-- erpnext/translations/el.csv | 8 +++-- erpnext/translations/es.csv | 60 ++++++++++++++++++---------------- erpnext/translations/es_ar.csv | 1 - erpnext/translations/es_gt.csv | 1 - erpnext/translations/et.csv | 8 +++-- erpnext/translations/fa.csv | 8 +++-- erpnext/translations/fi.csv | 8 +++-- erpnext/translations/fr.csv | 10 ++++-- erpnext/translations/gu.csv | 8 +++-- erpnext/translations/he.csv | 8 +++-- erpnext/translations/hi.csv | 8 +++-- erpnext/translations/hr.csv | 8 +++-- erpnext/translations/hu.csv | 8 +++-- erpnext/translations/id.csv | 8 +++-- erpnext/translations/is.csv | 8 +++-- erpnext/translations/it.csv | 8 +++-- erpnext/translations/ja.csv | 8 +++-- erpnext/translations/km.csv | 8 +++-- erpnext/translations/kn.csv | 8 +++-- erpnext/translations/ko.csv | 8 +++-- erpnext/translations/ku.csv | 8 +++-- erpnext/translations/lo.csv | 8 +++-- erpnext/translations/lt.csv | 8 +++-- erpnext/translations/lv.csv | 8 +++-- erpnext/translations/mk.csv | 8 +++-- erpnext/translations/ml.csv | 8 +++-- erpnext/translations/mr.csv | 8 +++-- erpnext/translations/ms.csv | 8 +++-- erpnext/translations/my.csv | 8 +++-- erpnext/translations/nl.csv | 8 +++-- erpnext/translations/no.csv | 8 +++-- erpnext/translations/pl.csv | 8 +++-- erpnext/translations/ps.csv | 8 +++-- erpnext/translations/pt.csv | 8 +++-- erpnext/translations/pt_br.csv | 1 + erpnext/translations/ro.csv | 8 +++-- erpnext/translations/ru.csv | 8 +++-- erpnext/translations/rw.csv | 8 +++-- erpnext/translations/si.csv | 8 +++-- erpnext/translations/sk.csv | 8 +++-- erpnext/translations/sl.csv | 8 +++-- erpnext/translations/sq.csv | 8 +++-- erpnext/translations/sr.csv | 8 +++-- erpnext/translations/sr_sp.csv | 1 - erpnext/translations/sv.csv | 8 +++-- erpnext/translations/sw.csv | 8 +++-- erpnext/translations/ta.csv | 8 +++-- erpnext/translations/te.csv | 8 +++-- erpnext/translations/th.csv | 8 +++-- erpnext/translations/tr.csv | 8 +++-- erpnext/translations/uk.csv | 8 +++-- erpnext/translations/ur.csv | 8 +++-- erpnext/translations/uz.csv | 8 +++-- erpnext/translations/vi.csv | 8 +++-- erpnext/translations/zh.csv | 8 +++-- erpnext/translations/zh_tw.csv | 8 +++-- 66 files changed, 400 insertions(+), 154 deletions(-) diff --git a/erpnext/translations/af.csv b/erpnext/translations/af.csv index 07f81e9979..45435d8600 100644 --- a/erpnext/translations/af.csv +++ b/erpnext/translations/af.csv @@ -4238,7 +4238,6 @@ Download as JSON,Laai af as Json, End date can not be less than start date,Einddatum kan nie minder wees as die begin datum nie, For Default Supplier (Optional),Vir Standaardverskaffer (opsioneel), From date cannot be greater than To date,Vanaf datum kan nie groter wees as Datum, -Get items from,Kry items van, Group by,Groep By, In stock,In voorraad, Item name,Item naam, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Aankooporders, Purchase Receipt Trends,Aankoopontvangstendense, Purchase Register,Aankoopregister, Quotation Trends,Aanhalingstendense, -Quoted Item Comparison,Genoteerde Item Vergelyking, Received Items To Be Billed,Items ontvang om gefaktureer te word, Qty to Order,Hoeveelheid om te bestel, Requested Items To Be Transferred,Gevraagde items wat oorgedra moet word, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Die item waarna verwys word Therapy Session overlaps with {0},Terapiesessie oorvleuel met {0}, Therapy Sessions Overlapping,Terapiesessies oorvleuel, Therapy Plans,Terapieplanne, +"Item Code, warehouse, quantity are required on row {0}","Itemkode, pakhuis, hoeveelheid word in ry {0} vereis", +Get Items from Material Requests against this Supplier,Kry items uit materiaalversoeke teen hierdie verskaffer, +Enable European Access,Aktiveer Europese toegang, +Creating Purchase Order ...,Skep tans bestelling ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Kies 'n verskaffer uit die verstekverskaffers van die onderstaande items. By seleksie sal 'n bestelling slegs gemaak word teen items wat tot die geselekteerde verskaffer behoort., +Row #{}: You must select {} serial numbers for item {}.,Ry # {}: u moet {} reeksnommers vir item {} kies., diff --git a/erpnext/translations/am.csv b/erpnext/translations/am.csv index 1d0533fd99..554b0a54f1 100644 --- a/erpnext/translations/am.csv +++ b/erpnext/translations/am.csv @@ -4238,7 +4238,6 @@ Download as JSON,እንደ ጆንሰን አውርድ ፡፡, End date can not be less than start date,የማብቂያ ቀን ከመጀመሪያ ቀን ያነሰ መሆን አይችልም, For Default Supplier (Optional),ነባሪ አቅራቢ (አማራጭ), From date cannot be greater than To date,ቀን ቀን ወደ በላይ ሊሆን አይችልም ከ, -Get items from,ከ ንጥሎችን ያግኙ, Group by,ቡድን በ, In stock,ለሽያጭ የቀረበ እቃ, Item name,ንጥል ስም, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ትዕዛዝ በመታየት ላይ ይግዙ, Purchase Receipt Trends,የግዢ ደረሰኝ በመታየት ላይ ያሉ, Purchase Register,የግዢ ይመዝገቡ, Quotation Trends,በትዕምርተ ጥቅስ አዝማሚያዎች, -Quoted Item Comparison,የተጠቀሰ ንጥል ንጽጽር, Received Items To Be Billed,ተቀብሏል ንጥሎች እንዲከፍሉ ለማድረግ, Qty to Order,ለማዘዝ ብዛት, Requested Items To Be Transferred,ተጠይቋል ንጥሎች መወሰድ, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,የተጠቀሰው ንጥ Therapy Session overlaps with {0},ቴራፒ ክፍለ-ጊዜ ከ {0} ጋር ይደራረባል, Therapy Sessions Overlapping,ቴራፒ ክፍለ-ጊዜዎች መደራረብ, Therapy Plans,የሕክምና ዕቅዶች, +"Item Code, warehouse, quantity are required on row {0}",የእቃ ኮድ ፣ መጋዘን ፣ ብዛት በረድፍ {0} ላይ ያስፈልጋሉ, +Get Items from Material Requests against this Supplier,በዚህ አቅራቢ ላይ እቃዎችን ከቁሳዊ ጥያቄዎች ያግኙ, +Enable European Access,የአውሮፓ መዳረሻን ያንቁ, +Creating Purchase Order ...,የግዢ ትዕዛዝ በመፍጠር ላይ ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",ከዚህ በታች ካሉ ዕቃዎች ነባሪ አቅራቢዎች አቅራቢ ይምረጡ። በምርጫ ወቅት ለተመረጠው አቅራቢ ብቻ በሆኑ ዕቃዎች ላይ የግዢ ትዕዛዝ ይደረጋል።, +Row #{}: You must select {} serial numbers for item {}.,ረድፍ # {}: {} ለንጥል ተከታታይ ቁጥሮች {} መምረጥ አለብዎት።, diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv index de75e893c7..91a9da9f16 100644 --- a/erpnext/translations/ar.csv +++ b/erpnext/translations/ar.csv @@ -4238,7 +4238,6 @@ Download as JSON,تنزيل باسم Json, End date can not be less than start date,تاريخ النهاية لا يمكن أن يكون اقل من تاريخ البدء\n
\nEnd Date can not be less than Start Date, For Default Supplier (Optional),للمورد الافتراضي (اختياري), From date cannot be greater than To date,(من تاريخ) لا يمكن أن يكون أكبر (الي التاريخ), -Get items from,الحصول على البنود من, Group by,المجموعة حسب, In stock,في المخزن, Item name,اسم السلعة, @@ -8549,7 +8548,6 @@ Purchase Order Trends,اتجهات امر الشراء, Purchase Receipt Trends,شراء اتجاهات الإيصال, Purchase Register,سجل شراء, Quotation Trends,مؤشرات المناقصة, -Quoted Item Comparison,مقارنة بند المناقصة, Received Items To Be Billed,العناصر الواردة إلى أن توصف, Qty to Order,الكمية للطلب, Requested Items To Be Transferred,العناصر المطلوبة على أن يتم تحويلها, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,العنصر المشار Therapy Session overlaps with {0},تتداخل جلسة العلاج مع {0}, Therapy Sessions Overlapping,جلسات العلاج متداخلة, Therapy Plans,خطط العلاج, +"Item Code, warehouse, quantity are required on row {0}",مطلوب رمز الصنف والمستودع والكمية في الصف {0}, +Get Items from Material Requests against this Supplier,الحصول على عناصر من طلبات المواد ضد هذا المورد, +Enable European Access,تمكين الوصول الأوروبي, +Creating Purchase Order ...,إنشاء أمر شراء ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",حدد موردًا من الموردين الافتراضيين للعناصر أدناه. عند التحديد ، سيتم إجراء طلب الشراء مقابل العناصر التي تنتمي إلى المورد المحدد فقط., +Row #{}: You must select {} serial numbers for item {}.,الصف # {}: يجب تحديد {} الأرقام التسلسلية للعنصر {}., diff --git a/erpnext/translations/bg.csv b/erpnext/translations/bg.csv index 79be6dfc19..15278a6a40 100644 --- a/erpnext/translations/bg.csv +++ b/erpnext/translations/bg.csv @@ -4238,7 +4238,6 @@ Download as JSON,Изтеглете като JSON, End date can not be less than start date,Крайна дата не може да бъде по-малка от началната дата, For Default Supplier (Optional),За доставчик по подразбиране (по избор), From date cannot be greater than To date,От дата не може да бъде по-голямо от до дата, -Get items from,Вземете елементи от, Group by,Групирай по, In stock,В наличност, Item name,Име на артикул, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Поръчката Trends, Purchase Receipt Trends,Покупка Квитанция Trends, Purchase Register,Покупка Регистрация, Quotation Trends,Оферта Тенденции, -Quoted Item Comparison,Сравнение на редове от оферти, Received Items To Be Billed,"Приети артикули, които да се фактирират", Qty to Order,Количество към поръчка, Requested Items To Be Transferred,Желани артикули да бъдат прехвърлени, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Позицията, пос Therapy Session overlaps with {0},Терапевтичната сесия се припокрива с {0}, Therapy Sessions Overlapping,Терапевтични сесии Припокриване, Therapy Plans,Планове за терапия, +"Item Code, warehouse, quantity are required on row {0}","Код на артикул, склад, количество се изискват на ред {0}", +Get Items from Material Requests against this Supplier,Вземете артикули от заявки за материали срещу този доставчик, +Enable European Access,Активирайте европейския достъп, +Creating Purchase Order ...,Създаване на поръчка ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Изберете доставчик от доставчиците по подразбиране на елементите по-долу. При избора ще бъде направена Поръчка за покупка срещу артикули, принадлежащи само на избрания Доставчик.", +Row #{}: You must select {} serial numbers for item {}.,Ред № {}: Трябва да изберете {} серийни номера за артикул {}., diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv index 1e7413b1cd..cf0971667b 100644 --- a/erpnext/translations/bn.csv +++ b/erpnext/translations/bn.csv @@ -4238,7 +4238,6 @@ Download as JSON,জসন হিসাবে ডাউনলোড করুন End date can not be less than start date,শেষ তারিখ শুরু তারিখ থেকে কম হতে পারে না, For Default Supplier (Optional),ডিফল্ট সরবরাহকারীর জন্য (ঐচ্ছিক), From date cannot be greater than To date,তারিখ থেকে তারিখের চেয়ে বেশি হতে পারে না, -Get items from,থেকে আইটেম পান, Group by,গ্রুপ দ্বারা, In stock,স্টক ইন, Item name,আইটেম নাম, @@ -8549,7 +8548,6 @@ Purchase Order Trends,অর্ডার প্রবণতা ক্রয়, Purchase Receipt Trends,কেনার রসিদ প্রবণতা, Purchase Register,ক্রয় নিবন্ধন, Quotation Trends,উদ্ধৃতি প্রবণতা, -Quoted Item Comparison,উদ্ধৃত আইটেম তুলনা, Received Items To Be Billed,গৃহীত চলছে বিল তৈরি করা, Qty to Order,অর্ডার Qty, Requested Items To Be Transferred,অনুরোধ করা চলছে স্থানান্তর করা, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} দ্বার Therapy Session overlaps with {0},থেরাপি সেশন {0 with দিয়ে ওভারল্যাপ করে, Therapy Sessions Overlapping,থেরাপি সেশনস ওভারল্যাপিং, Therapy Plans,থেরাপি পরিকল্পনা, +"Item Code, warehouse, quantity are required on row {0}","আইটেম কোড, গুদাম, পরিমাণ সারিতে প্রয়োজন {0}", +Get Items from Material Requests against this Supplier,এই সরবরাহকারীর বিরুদ্ধে উপাদান অনুরোধগুলি থেকে আইটেমগুলি পান, +Enable European Access,ইউরোপীয় অ্যাক্সেস সক্ষম করুন, +Creating Purchase Order ...,ক্রয় ক্রম তৈরি করা হচ্ছে ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","নীচের আইটেমগুলির ডিফল্ট সরবরাহকারী থেকে কোনও সরবরাহকারী নির্বাচন করুন। নির্বাচনের সময়, কেবলমাত্র নির্বাচিত সরবরাহকারীর অন্তর্ভুক্ত আইটেমগুলির বিরুদ্ধে ক্রয় আদেশ দেওয়া হবে।", +Row #{}: You must select {} serial numbers for item {}.,সারি # {}: আইটেমের জন্য আপনাকে অবশ্যই}} ক্রমিক সংখ্যা নির্বাচন করতে হবে {}।, diff --git a/erpnext/translations/bs.csv b/erpnext/translations/bs.csv index 4fec5e0cc8..6ef445a1af 100644 --- a/erpnext/translations/bs.csv +++ b/erpnext/translations/bs.csv @@ -4238,7 +4238,6 @@ Download as JSON,Preuzmi kao JSON, End date can not be less than start date,Datum završetka ne može biti manja od početnog datuma, For Default Supplier (Optional),Za podrazumevani dobavljač, From date cannot be greater than To date,Od datuma ne može biti veća od To Date, -Get items from,Get stavke iz, Group by,Group By, In stock,Na zalihama, Item name,Naziv artikla, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Trendovi narudžbenica kupnje, Purchase Receipt Trends,Račun kupnje trendovi, Purchase Register,Kupnja Registracija, Quotation Trends,Trendovi ponude, -Quoted Item Comparison,Citirano Stavka Poređenje, Received Items To Be Billed,Primljeni Proizvodi se naplaćuje, Qty to Order,Količina za narudžbu, Requested Items To Be Transferred,Traženi stavki za prijenos, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Stavka na koju se poziva {0 Therapy Session overlaps with {0},Sjednica terapije preklapa se sa {0}, Therapy Sessions Overlapping,Preklapanje terapijskih sesija, Therapy Plans,Planovi terapije, +"Item Code, warehouse, quantity are required on row {0}","Šifra artikla, skladište, količina su obavezni na retku {0}", +Get Items from Material Requests against this Supplier,Nabavite predmete od materijalnih zahtjeva protiv ovog dobavljača, +Enable European Access,Omogućiti evropski pristup, +Creating Purchase Order ...,Kreiranje narudžbenice ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Odaberite dobavljača od zadanih dobavljača dolje navedenih stavki. Nakon odabira, narudžbenica će se izvršiti samo za proizvode koji pripadaju odabranom dobavljaču.", +Row #{}: You must select {} serial numbers for item {}.,Red # {}: Morate odabrati {} serijske brojeve za stavku {}., diff --git a/erpnext/translations/ca.csv b/erpnext/translations/ca.csv index 58e6edb0a8..18fa52a2b7 100644 --- a/erpnext/translations/ca.csv +++ b/erpnext/translations/ca.csv @@ -4238,7 +4238,6 @@ Download as JSON,Descarregueu com a Json, End date can not be less than start date,Data de finalització no pot ser inferior a data d'inici, For Default Supplier (Optional),Per proveïdor predeterminat (opcional), From date cannot be greater than To date,Des de la data no pot ser superior a la data, -Get items from,Obtenir articles de, Group by,Agrupar per, In stock,En estoc, Item name,Nom de l'article, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Compra Tendències Sol·licitar, Purchase Receipt Trends,Purchase Receipt Trends, Purchase Register,Compra de Registre, Quotation Trends,Quotation Trends, -Quoted Item Comparison,Citat article Comparació, Received Items To Be Billed,Articles rebuts per a facturar, Qty to Order,Quantitat de comanda, Requested Items To Be Transferred,Articles sol·licitats per a ser transferits, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,L'element a què fa ref Therapy Session overlaps with {0},La sessió de teràpia es coincideix amb {0}, Therapy Sessions Overlapping,Sessions de teràpia superposades, Therapy Plans,Plans de teràpia, +"Item Code, warehouse, quantity are required on row {0}","El codi de l'article, el magatzem, la quantitat són obligatoris a la fila {0}", +Get Items from Material Requests against this Supplier,Obteniu articles de sol·licituds de material contra aquest proveïdor, +Enable European Access,Activa l'accés europeu, +Creating Purchase Order ...,S'està creant una comanda de compra ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Seleccioneu un proveïdor dels proveïdors predeterminats dels articles següents. En seleccionar-lo, es farà una Comanda de compra únicament contra articles pertanyents al Proveïdor seleccionat.", +Row #{}: You must select {} serial numbers for item {}.,Fila núm. {}: Heu de seleccionar {} números de sèrie de l'element {}., diff --git a/erpnext/translations/cs.csv b/erpnext/translations/cs.csv index 43fd472b41..705e471d27 100644 --- a/erpnext/translations/cs.csv +++ b/erpnext/translations/cs.csv @@ -4238,7 +4238,6 @@ Download as JSON,Stáhnout jako JSON, End date can not be less than start date,Datum ukončení nesmí být menší než datum zahájení, For Default Supplier (Optional),Výchozí dodavatel (volitelné), From date cannot be greater than To date,Od Datum nemůže být větší než Datum, -Get items from,Položka získaná z, Group by,Seskupit podle, In stock,Na skladě, Item name,Název položky, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Nákupní objednávka trendy, Purchase Receipt Trends,Doklad o koupi Trendy, Purchase Register,Nákup Register, Quotation Trends,Uvozovky Trendy, -Quoted Item Comparison,Citoval Položka Porovnání, Received Items To Be Billed,"Přijaté položek, které mají být účtovány", Qty to Order,Množství k objednávce, Requested Items To Be Transferred,Požadované položky mají být převedeny, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Položka, na kterou odkazu Therapy Session overlaps with {0},Terapie se překrývá s {0}, Therapy Sessions Overlapping,Terapeutické relace se překrývají, Therapy Plans,Terapeutické plány, +"Item Code, warehouse, quantity are required on row {0}","Na řádku {0} je vyžadován kód položky, sklad, množství", +Get Items from Material Requests against this Supplier,Získejte položky z materiálových požadavků vůči tomuto dodavateli, +Enable European Access,Povolit evropský přístup, +Creating Purchase Order ...,Vytváření nákupní objednávky ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Vyberte dodavatele z výchozích dodavatelů níže uvedených položek. Při výběru bude provedena objednávka pouze na položky patřící vybranému dodavateli., +Row #{}: You must select {} serial numbers for item {}.,Řádek # {}: Musíte vybrat {} sériová čísla pro položku {}., diff --git a/erpnext/translations/da.csv b/erpnext/translations/da.csv index 7222daf77f..c0d0146694 100644 --- a/erpnext/translations/da.csv +++ b/erpnext/translations/da.csv @@ -4238,7 +4238,6 @@ Download as JSON,Download som JSON, End date can not be less than start date,Slutdato kan ikke være mindre end startdato, For Default Supplier (Optional),For standardleverandør (valgfrit), From date cannot be greater than To date,Fra dato ikke kan være større end til dato, -Get items from,Hent varer fra, Group by,Sortér efter, In stock,På lager, Item name,Varenavn, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Indkøbsordre Trends, Purchase Receipt Trends,Købskvittering Tendenser, Purchase Register,Indkøb Register, Quotation Trends,Tilbud trends, -Quoted Item Comparison,Sammenligning Citeret Vare, Received Items To Be Billed,Modtagne varer skal faktureres, Qty to Order,Antal til ordre, Requested Items To Be Transferred,"Anmodet Varer, der skal overføres", @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Den vare, der henvises til Therapy Session overlaps with {0},Terapisession overlapper med {0}, Therapy Sessions Overlapping,Terapisessioner overlapper hinanden, Therapy Plans,Terapiplaner, +"Item Code, warehouse, quantity are required on row {0}","Varekode, lager, antal kræves i række {0}", +Get Items from Material Requests against this Supplier,Få varer fra materialeanmodninger mod denne leverandør, +Enable European Access,Aktiver europæisk adgang, +Creating Purchase Order ...,Opretter indkøbsordre ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Vælg en leverandør fra standardleverandørerne af nedenstående varer. Ved valg foretages en indkøbsordre kun mod varer, der tilhører den valgte leverandør.", +Row #{}: You must select {} serial numbers for item {}.,Række nr. {}: Du skal vælge {} serienumre for varen {}., diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index b65c9436ef..ca03a787cd 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -4238,7 +4238,6 @@ Download as JSON,Als JSON herunterladen, End date can not be less than start date,Das Enddatum darf nicht kleiner als das Startdatum sein, For Default Supplier (Optional),Für Standardlieferanten (optional), From date cannot be greater than To date,Das Ab-Datum kann nicht größer als das Bis-Datum sein, -Get items from,Holen Sie Elemente aus, Group by,Gruppieren nach, In stock,Auf Lager, Item name,Artikelname, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Entwicklung Lieferantenaufträge, Purchase Receipt Trends,Trendanalyse Kaufbelege, Purchase Register,Übersicht über Einkäufe, Quotation Trends,Trendanalyse Angebote, -Quoted Item Comparison,Vergleich angebotener Artikel, Received Items To Be Billed,"Von Lieferanten gelieferte Artikel, die noch abgerechnet werden müssen", Qty to Order,Zu bestellende Menge, Requested Items To Be Transferred,"Angeforderte Artikel, die übertragen werden sollen", @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Der Artikel, auf den {0} - Therapy Session overlaps with {0},Die Therapiesitzung überschneidet sich mit {0}, Therapy Sessions Overlapping,Überlappende Therapiesitzungen, Therapy Plans,Therapiepläne, +"Item Code, warehouse, quantity are required on row {0}","Artikelcode, Lager, Menge sind in Zeile {0} erforderlich.", +Get Items from Material Requests against this Supplier,Erhalten Sie Artikel aus Materialanfragen gegen diesen Lieferanten, +Enable European Access,Ermöglichen Sie den europäischen Zugang, +Creating Purchase Order ...,Bestellung anlegen ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Wählen Sie einen Lieferanten aus den Standardlieferanten der folgenden Artikel aus. Bei der Auswahl erfolgt eine Bestellung nur für Artikel, die dem ausgewählten Lieferanten gehören.", +Row #{}: You must select {} serial numbers for item {}.,Zeile # {}: Sie müssen {} Seriennummern für Artikel {} auswählen., diff --git a/erpnext/translations/el.csv b/erpnext/translations/el.csv index 2db4ac8c1c..acf5db5d46 100644 --- a/erpnext/translations/el.csv +++ b/erpnext/translations/el.csv @@ -4238,7 +4238,6 @@ Download as JSON,Λήψη ως JSON, End date can not be less than start date,Η ημερομηνία λήξης δεν μπορεί να είναι προγενέστερη της ημερομηνίας έναρξης, For Default Supplier (Optional),Για προεπιλεγμένο προμηθευτή (προαιρετικό), From date cannot be greater than To date,Από την ημερομηνία αυτή δεν μπορεί να είναι μεταγενέστερη από την έως ημερομηνία, -Get items from,Πάρτε τα στοιχεία από, Group by,Ομαδοποίηση κατά, In stock,Σε απόθεμα, Item name,Όνομα είδους, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Τάσεις παραγγελίας αγοράς, Purchase Receipt Trends,Τάσεις αποδεικτικού παραλαβής αγοράς, Purchase Register,Ταμείο αγορών, Quotation Trends,Τάσεις προσφορών, -Quoted Item Comparison,Εισηγμένες Στοιχείο Σύγκριση, Received Items To Be Billed,Είδη που παραλήφθηκαν και πρέπει να τιμολογηθούν, Qty to Order,Ποσότητα για παραγγελία, Requested Items To Be Transferred,Είδη που ζητήθηκε να μεταφερθούν, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Το στοιχείο πο Therapy Session overlaps with {0},Η συνεδρία θεραπείας αλληλεπικαλύπτεται με {0}, Therapy Sessions Overlapping,Συνεδρίες συνεδρίας, Therapy Plans,Σχέδια θεραπείας, +"Item Code, warehouse, quantity are required on row {0}","Κωδικός είδους, αποθήκη, ποσότητα απαιτείται στη σειρά {0}", +Get Items from Material Requests against this Supplier,Λάβετε στοιχεία από αιτήματα υλικών έναντι αυτού του προμηθευτή, +Enable European Access,Ενεργοποίηση ευρωπαϊκής πρόσβασης, +Creating Purchase Order ...,Δημιουργία παραγγελίας αγοράς ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Επιλέξτε έναν προμηθευτή από τους προεπιλεγμένους προμηθευτές των παρακάτω στοιχείων. Κατά την επιλογή, μια εντολή αγοράς θα πραγματοποιείται έναντι αντικειμένων που ανήκουν στον επιλεγμένο Προμηθευτή μόνο.", +Row #{}: You must select {} serial numbers for item {}.,Σειρά # {}: Πρέπει να επιλέξετε {} σειριακούς αριθμούς για το στοιχείο {}., diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index be195aad11..0f2259db21 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -421,7 +421,7 @@ Buildings,Edificios, Bundle items at time of sale.,Agrupe elementos al momento de la venta., Business Development Manager,Gerente de Desarrollo de Negocios, Buy,Comprar, -Buying,Comprando, +Buying,Compras, Buying Amount,Importe de compra, Buying Price List,Lista de precios de compra, Buying Rate,Tipo de Cambio de Compra, @@ -535,7 +535,7 @@ City/Town,Ciudad / Provincia, Claimed Amount,Cantidad reclamada, Clay,Arcilla, Clear filters,Filtros claros, -Clear values,Valores claros, +Clear values,Quitar valores, Clearance Date,Fecha de liquidación, Clearance Date not mentioned,Fecha de liquidación no definida, Clearance Date updated,Fecha de liquidación actualizada, @@ -728,7 +728,7 @@ Current Liabilities,Pasivo circulante, Current Qty,Cant. Actual, Current invoice {0} is missing,La factura actual {0} falta, Custom HTML,HTML Personalizado, -Custom?,Personalizado?, +Custom?,¿Personalizado?, Customer,Cliente, Customer Addresses And Contacts,Direcciones de clientes y contactos, Customer Contact,Contacto del Cliente, @@ -783,7 +783,7 @@ Default Activity Cost exists for Activity Type - {0},Existe una actividad de cos Default BOM ({0}) must be active for this item or its template,La lista de materiales (LdM) por defecto ({0}) debe estar activa para este producto o plantilla, Default BOM for {0} not found,BOM por defecto para {0} no encontrado, Default BOM not found for Item {0} and Project {1},La lista de materiales predeterminada no se encontró para el Elemento {0} y el Proyecto {1}, -Default Letter Head,Encabezado predeterminado, +Default Letter Head,Encabezado Predeterminado, Default Tax Template,Plantilla de impuesto predeterminado, Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,Unidad de medida predeterminada para el artículo {0} no se puede cambiar directamente porque ya ha realizado alguna transacción (s) con otra UOM. Usted tendrá que crear un nuevo elemento a utilizar un UOM predeterminado diferente., Default Unit of Measure for Variant '{0}' must be same as in Template '{1}',Unidad de medida predeterminada para variante '{0}' debe ser la mismo que en la plantilla '{1}', @@ -966,7 +966,7 @@ Equity,Patrimonio, Error Log,Registro de Errores, Error evaluating the criteria formula,Error al evaluar la fórmula de criterios, Error in formula or condition: {0},Error Fórmula o Condición: {0}, -Error: Not a valid id?,Error: No es un ID válido?, +Error: Not a valid id?,Error: ¿No es un ID válido?, Estimated Cost,Costo estimado, Evaluation,Evaluación, "Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:","Incluso si hay varias reglas de precios con mayor prioridad, se aplican entonces siguientes prioridades internas:", @@ -1338,7 +1338,7 @@ Issue Material,Distribuir materiales, Issued,Emitido, Issues,Incidencias, It is needed to fetch Item Details.,Se necesita a buscar Detalles del artículo., -Item,Productos, +Item,Producto, Item 1,Elemento 1, Item 2,Elemento 2, Item 3,Elemento 3, @@ -1426,7 +1426,7 @@ Last Purchase Price,Último precio de compra, Last Purchase Rate,Tasa de cambio de última compra, Latest,Más reciente, Latest price updated in all BOMs,Último precio actualizado en todas las Listas de Materiales, -Lead,Dirigir, +Lead,Iniciativa, Lead Count,Cuenta de Iniciativa, Lead Owner,Propietario de la iniciativa, Lead Owner cannot be same as the Lead,Propietario de Iniciativa no puede ser igual que el de la Iniciativa, @@ -1535,7 +1535,7 @@ Mark Attendance,Marcar Asistencia, Mark Half Day,Marcar medio día, Mark Present,Marcar Presente, Marketing,Márketing, -Marketing Expenses,GASTOS DE PUBLICIDAD, +Marketing Expenses,Gastos de Publicidad, Marketplace,Mercado, Marketplace Error,Error de Marketplace, Masters,Maestros, @@ -1867,7 +1867,7 @@ Parents Teacher Meeting Attendance,Padres Maestros Asistencia a la Reunión, Part-time,Tiempo parcial, Partially Depreciated,Despreciables Parcialmente, Partially Received,Parcialmente recibido, -Party,Partido, +Party,Tercero, Party Name,Nombre de Parte, Party Type,Tipo de entidad, Party Type and Party is mandatory for {0} account,Tipo de Tercero y Tercero es obligatorio para la Cuenta {0}, @@ -2027,7 +2027,7 @@ Please select Company first,"Por favor, seleccione primero la compañía", Please select Completion Date for Completed Asset Maintenance Log,Seleccione Fecha de Finalización para el Registro de Mantenimiento de Activos Completado, Please select Completion Date for Completed Repair,Seleccione Fecha de Finalización para la Reparación Completa, Please select Course,Por favor seleccione Curso, -Please select Drug,Seleccione Droga, +Please select Drug,Por favor seleccione fármaco, Please select Employee,Por favor selecciona Empleado, Please select Existing Company for creating Chart of Accounts,"Por favor, seleccione empresa ya existente para la creación del plan de cuentas", Please select Healthcare Service,Por favor seleccione Servicio de Salud, @@ -2118,7 +2118,7 @@ Please specify from/to range,"Por favor, especifique el rango (desde / hasta)", Please supply the specified items at the best possible rates,Por favor suministrar los elementos especificados en las mejores tasas posibles, Please update your status for this training event,Actualice su estado para este evento de capacitación., Please wait 3 days before resending the reminder.,Espere 3 días antes de volver a enviar el recordatorio., -Point of Sale,Punto de venta, +Point of Sale,Punto de Venta, Point-of-Sale,Punto de Venta (POS), Point-of-Sale Profile,Perfiles de punto de venta (POS), Portal,Portal, @@ -2627,7 +2627,7 @@ Sell,Vender, Selling,Ventas, Selling Amount,Cantidad de venta, Selling Price List,Lista de precios de venta, -Selling Rate,Tasa de ventas, +Selling Rate,Precio de venta, "Selling must be checked, if Applicable For is selected as {0}","'Ventas' debe ser seleccionada, si la opción: 'Aplicable para' esta seleccionado como {0}", Send Grant Review Email,Enviar correo electrónico de revisión de subvención, Send Now,Enviar ahora, @@ -2700,9 +2700,9 @@ Setup cheque dimensions for printing,Configurar dimensiones de cheque para la im Setup default values for POS Invoices,Configurar los valores predeterminados para facturas de POS, Setup mode of POS (Online / Offline),Modo de configuración de POS (Online / Offline), Setup your Institute in ERPNext,Configura tu instituto en ERPNext, -Share Balance,Compartir Saldo, +Share Balance,Balance de Acciones, Share Ledger,Share Ledger, -Share Management,Gestión de Compartir, +Share Management,Administración de Acciones, Share Transfer,Transferir Acciones, Share Type,Tipo de acción, Shareholder,Accionista, @@ -3302,7 +3302,7 @@ Where manufacturing operations are carried.,Dónde se realizan las operaciones d White,Blanco, Wire Transfer,Transferencia bancaria, WooCommerce Products,Productos WooCommerce, -Work In Progress,Trabajo en progreso, +Work In Progress,Trabajo en Proceso, Work Order,Orden de trabajo, Work Order already created for all items with BOM,Órden de Trabajo ya creada para todos los artículos con lista de materiales, Work Order cannot be raised against a Item Template,La Órden de Trabajo no puede levantarse contra una Plantilla de Artículo, @@ -3582,7 +3582,7 @@ Accounting Masters,Maestros Contables, Accounting Period overlaps with {0},El período contable se superpone con {0}, Activity,Actividad, Add / Manage Email Accounts.,Añadir / Administrar cuentas de correo electrónico., -Add Child,Agregar niño, +Add Child,Agregar hijo, Add Loan Security,Agregar seguridad de préstamo, Add Multiple,Añadir Multiple, Add Participants,Agregar Participantes, @@ -3706,7 +3706,7 @@ Description,Descripción, Designation,Puesto, Difference Value,Valor de diferencia, Dimension Filter,Filtro de dimensiones, -Disabled,Discapacitado, +Disabled,Deshabilitado, Disbursement and Repayment,Desembolso y reembolso, Distance cannot be greater than 4000 kms,La distancia no puede ser mayor a 4000 kms, Do you want to submit the material request,¿Quieres enviar la solicitud de material?, @@ -4193,7 +4193,7 @@ Total Income This Year,Ingresos totales este año, Barcode,Código de barras, Bold,Negrita, Center,Centro, -Clear,Claro, +Clear,Quitar, Comment,Comentario, Comments,Comentarios, DocType,DocType, @@ -4238,7 +4238,6 @@ Download as JSON,Descargar como JSON, End date can not be less than start date,la fecha final no puede ser inferior a fecha de Inicio, For Default Supplier (Optional),Para el proveedor predeterminado (opcional), From date cannot be greater than To date,La fecha 'Desde' no puede ser mayor que la fecha 'Hasta', -Get items from,Obtener artículos de, Group by,Agrupar por, In stock,En stock, Item name,Nombre del producto, @@ -4305,7 +4304,7 @@ Assets not created for {0}. You will have to create asset manually.,Activos no c Invalid Account,Cuenta no válida, Purchase Order Required,Orden de compra requerida, Purchase Receipt Required,Recibo de compra requerido, -Account Missing,Falta la cuenta, +Account Missing,Cuenta Faltante, Requested,Solicitado, Partially Paid,Parcialmente pagado, Invalid Account Currency,Moneda de la cuenta no válida, @@ -5600,7 +5599,7 @@ Call Log,Registro de llamadas, Received By,Recibido por, Caller Information,Información de la llamada, Contact Name,Nombre de contacto, -Lead ,Dirigir, +Lead ,Iniciativa, Lead Name,Nombre de la iniciativa, Ringing,Zumbido, Missed,Perdido, @@ -6619,7 +6618,7 @@ Employee External Work History,Historial de de trabajos anteriores, Total Experience,Experiencia total, Default Leave Policy,Política de Licencia Predeterminada, Default Salary Structure,Estructura de Salario Predeterminada, -Employee Group Table,Mesa de grupo de empleados, +Employee Group Table,Tabla de grupo de empleados, ERPNext User ID,ERP ID de usuario siguiente, Employee Health Insurance,Seguro de Salud para Empleados, Health Insurance Name,Nombre del Seguro de Salud, @@ -6838,10 +6837,10 @@ Employees,Empleados, Number Of Employees,Número de Empleados, Employee Details,Detalles del Empleado, Validate Attendance,Validar la Asistencia, -Salary Slip Based on Timesheet,Nomina basada en el Parte de Horas, +Salary Slip Based on Timesheet,Nomina basada horas, Select Payroll Period,Seleccione el Período de Nómina, Deduct Tax For Unclaimed Employee Benefits,Deducir Impuestos para beneficios de Empleados no Reclamados, -Deduct Tax For Unsubmitted Tax Exemption Proof,Deducir impuestos por prueba de exención de impuestos sin enviar, +Deduct Tax For Unsubmitted Tax Exemption Proof,Deducir impuestos por soporte de exención de impuestos sin enviar, Select Payment Account to make Bank Entry,Seleccionar la cuenta de pago para hacer la entrada del Banco, Salary Slips Created,Salario Slips creado, Salary Slips Submitted,Nómina Salarial Validada, @@ -7093,7 +7092,7 @@ Total Amount Paid,Cantidad Total Pagada, Loan Manager,Gerente de préstamos, Loan Info,Información del Préstamo, Rate of Interest,Tasa de interés, -Proposed Pledges,Promesas Propuestas, +Proposed Pledges,Prendas Propuestas, Maximum Loan Amount,Cantidad máxima del préstamo, Repayment Info,Información de la Devolución, Total Payable Interest,Interés Total a Pagar, @@ -7340,7 +7339,7 @@ Available Qty at Source Warehouse,Cantidad Disponible en Almacén Fuente, Available Qty at WIP Warehouse,Cantidad Disponible en Almacén WIP, Work Order Operation,Operación de Órden de Trabajo, Operation Description,Descripción de la operación, -Operation completed for how many finished goods?,Se completo la operación para la cantidad de productos terminados?, +Operation completed for how many finished goods?,¿Operación completada para cuántos productos terminados?, Work in Progress,Trabajo en proceso, Estimated Time and Cost,Tiempo estimado y costo, Planned Start Time,Hora prevista de inicio, @@ -8452,7 +8451,7 @@ Asset Depreciation Ledger,Libro Mayor Depreciacion de Activos, Asset Depreciations and Balances,Depreciaciones de Activos y Saldos, Available Stock for Packing Items,Inventario Disponible de Artículos de Embalaje, Bank Clearance Summary,Resumen de Cambios Bancarios, -Bank Remittance,Remesa bancaria, +Bank Remittance,Giro Bancario, Batch Item Expiry Status,Estado de Caducidad de Lote de Productos, Batch-Wise Balance History,Historial de Saldo por Lotes, BOM Explorer,BOM Explorer, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Tendencias de ordenes de compra, Purchase Receipt Trends,Tendencias de recibos de compra, Purchase Register,Registro de compras, Quotation Trends,Tendencias de Presupuestos, -Quoted Item Comparison,Comparación de artículos de Cotización, Received Items To Be Billed,Recepciones por facturar, Qty to Order,Cantidad a Solicitar, Requested Items To Be Transferred,Artículos solicitados para ser transferidos, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,El artículo al que hace re Therapy Session overlaps with {0},La sesión de terapia se superpone con {0}, Therapy Sessions Overlapping,Superposición de sesiones de terapia, Therapy Plans,Planes de terapia, +"Item Code, warehouse, quantity are required on row {0}","El código de artículo, el almacén y la cantidad son obligatorios en la fila {0}", +Get Items from Material Requests against this Supplier,Obtener artículos de solicitudes de material contra este proveedor, +Enable European Access,Habilitar el acceso europeo, +Creating Purchase Order ...,Creando orden de compra ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Seleccione un proveedor de los proveedores predeterminados de los artículos a continuación. En la selección, se realizará una orden de compra contra los artículos que pertenecen al proveedor seleccionado únicamente.", +Row #{}: You must select {} serial numbers for item {}.,Fila # {}: debe seleccionar {} números de serie para el artículo {}., diff --git a/erpnext/translations/es_ar.csv b/erpnext/translations/es_ar.csv index 9bdcba1195..1fd87236f1 100644 --- a/erpnext/translations/es_ar.csv +++ b/erpnext/translations/es_ar.csv @@ -1,5 +1,4 @@ Employee {0} on Half day on {1},"Empleado {0}, media jornada el día {1}", -Item,Producto, Communication,Comunicacion, Components,Componentes, Cheque Size,Tamaño de Cheque, diff --git a/erpnext/translations/es_gt.csv b/erpnext/translations/es_gt.csv index efd2daaa31..29ce2b8a87 100644 --- a/erpnext/translations/es_gt.csv +++ b/erpnext/translations/es_gt.csv @@ -1,4 +1,3 @@ -Item,Producto, Lead Time Days,Tiempo de ejecución en días, Outstanding,Pendiente, Outstanding Amount,Saldo Pendiente, diff --git a/erpnext/translations/et.csv b/erpnext/translations/et.csv index b778eff4d6..ba32187c68 100644 --- a/erpnext/translations/et.csv +++ b/erpnext/translations/et.csv @@ -4238,7 +4238,6 @@ Download as JSON,Laadige alla kui Json, End date can not be less than start date,End Date saa olla väiksem kui alguskuupäev, For Default Supplier (Optional),Vaikimisi tarnija (valikuline), From date cannot be greater than To date,Siit kuupäev ei saa olla suurem kui kuupäev, -Get items from,Võta esemed, Group by,Group By, In stock,Laos, Item name,Toote nimi, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Ostutellimuse Trends, Purchase Receipt Trends,Ostutšekk Trends, Purchase Register,Ostu Registreeri, Quotation Trends,Tsitaat Trends, -Quoted Item Comparison,Tsiteeritud Punkt võrdlus, Received Items To Be Billed,Saadud objekte arve, Qty to Order,Kogus tellida, Requested Items To Be Transferred,Taotletud üleantavate, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Üksus, millele viitab {0} Therapy Session overlaps with {0},Teraapiaseanss kattub rakendusega {0}, Therapy Sessions Overlapping,Teraapiaseansid kattuvad, Therapy Plans,Teraapiakavad, +"Item Code, warehouse, quantity are required on row {0}","Real {0} on nõutav kaubakood, ladu ja kogus", +Get Items from Material Requests against this Supplier,Hankige esemeid selle tarnija vastu esitatud materjalitaotlustest, +Enable European Access,Luba Euroopa juurdepääs, +Creating Purchase Order ...,Ostutellimuse loomine ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Valige allpool olevate üksuste vaiketarnijatest tarnija. Valiku alusel tehakse ostutellimus ainult valitud tarnijale kuuluvate kaupade kohta., +Row #{}: You must select {} serial numbers for item {}.,Rida nr {}: peate valima {} üksuse seerianumbrid {}., diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv index 0e4d19a01b..4a7c979499 100644 --- a/erpnext/translations/fa.csv +++ b/erpnext/translations/fa.csv @@ -4238,7 +4238,6 @@ Download as JSON,به عنوان JSON بارگیری کنید, End date can not be less than start date,تاریخ پایان نمی تواند کمتر از تاریخ شروع, For Default Supplier (Optional),برای تامین کننده پیش فرض (اختیاری), From date cannot be greater than To date,از تاریخ نمی تواند بیشتر از تاریخ باشد, -Get items from,گرفتن اقلام از, Group by,گروه توسط, In stock,در انبار, Item name,نام آیتم, @@ -8549,7 +8548,6 @@ Purchase Order Trends,خرید سفارش روند, Purchase Receipt Trends,روند رسید خرید, Purchase Register,خرید ثبت نام, Quotation Trends,روند نقل قول, -Quoted Item Comparison,مورد نقل مقایسه, Received Items To Be Billed,دریافت گزینه هایی که صورتحساب, Qty to Order,تعداد سفارش, Requested Items To Be Transferred,آیتم ها درخواست می شود منتقل, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,مورد ارجاع شده Therapy Session overlaps with {0},جلسه درمانی با {0} همپوشانی دارد, Therapy Sessions Overlapping,جلسات درمانی با هم تداخل دارند, Therapy Plans,برنامه های درمانی, +"Item Code, warehouse, quantity are required on row {0}",کد مورد ، انبار ، مقدار در ردیف {0} لازم است, +Get Items from Material Requests against this Supplier,مواردی را از درخواستهای ماده در برابر این تأمین کننده دریافت کنید, +Enable European Access,دسترسی اروپا را فعال کنید, +Creating Purchase Order ...,در حال ایجاد سفارش خرید ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",از تامین کنندگان پیش فرض موارد زیر یک تأمین کننده انتخاب کنید. هنگام انتخاب ، سفارش خرید فقط در مورد کالاهای متعلق به تنها تامین کننده انتخاب شده انجام می شود., +Row #{}: You must select {} serial numbers for item {}.,ردیف شماره {}: شما باید {} شماره سریال را برای مورد {} انتخاب کنید., diff --git a/erpnext/translations/fi.csv b/erpnext/translations/fi.csv index 23fd7a7276..29eb56702d 100644 --- a/erpnext/translations/fi.csv +++ b/erpnext/translations/fi.csv @@ -4238,7 +4238,6 @@ Download as JSON,Lataa nimellä JSON, End date can not be less than start date,päättymispäivä ei voi olla ennen aloituspäivää, For Default Supplier (Optional),Oletuksena toimittaja (valinnainen), From date cannot be greater than To date,Alkaen päivä ei voi olla suurempi kuin päättymispäivä, -Get items from,Hae nimikkeet, Group by,ryhmän, In stock,Varastossa, Item name,Nimikkeen nimi, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Ostotilausten kehitys, Purchase Receipt Trends,Saapumisten kehitys, Purchase Register,Osto Rekisteröidy, Quotation Trends,Tarjousten kehitys, -Quoted Item Comparison,Noteeratut Kohta Vertailu, Received Items To Be Billed,Saivat kohteet laskuttamat, Qty to Order,Tilattava yksikkömäärä, Requested Items To Be Transferred,siirrettävät pyydetyt tuotteet, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Kohde, johon {0} - {1} vii Therapy Session overlaps with {0},Hoitoistunto on päällekkäinen kohteen {0} kanssa, Therapy Sessions Overlapping,Hoitoistunnot ovat päällekkäisiä, Therapy Plans,Hoitosuunnitelmat, +"Item Code, warehouse, quantity are required on row {0}","Tuotekoodi, varasto, määrä vaaditaan rivillä {0}", +Get Items from Material Requests against this Supplier,Hanki tuotteita tältä toimittajalta saaduista materiaalipyynnöistä, +Enable European Access,Ota käyttöön eurooppalainen käyttöoikeus, +Creating Purchase Order ...,Luodaan ostotilausta ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Valitse toimittaja alla olevien tuotteiden oletustoimittajista. Valinnan yhteydessä ostotilaus tehdään vain valitulle toimittajalle kuuluvista tuotteista., +Row #{}: You must select {} serial numbers for item {}.,Rivi # {}: Sinun on valittava {} tuotteen sarjanumerot {}., diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 09a90331c7..3cdae454ab 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -583,7 +583,7 @@ Condition,Conditions, Configure,Configurer, Configure {0},Configurer {0}, Confirmed orders from Customers.,Commandes confirmées des clients., -Connect Amazon with ERPNext,Connectez Amazon avec ERPNext, +Connect Amazon with ERPNext,Connecter Amazon avec ERPNext, Connect Shopify with ERPNext,Connectez Shopify avec ERPNext, Connect to Quickbooks,Se connecter à Quickbooks, Connected to QuickBooks,Connecté à QuickBooks, @@ -4238,7 +4238,6 @@ Download as JSON,Télécharger en JSON, End date can not be less than start date,La date de Fin ne peut pas être antérieure à la Date de Début, For Default Supplier (Optional),Pour le fournisseur par défaut (facultatif), From date cannot be greater than To date,La Date Initiale ne peut pas être postérieure à la Date Finale, -Get items from,Obtenir les articles de, Group by,Grouper Par, In stock,En stock, Item name,Nom de l'article, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Tendances des Bons de Commande, Purchase Receipt Trends,Tendances des Reçus d'Achats, Purchase Register,Registre des Achats, Quotation Trends,Tendances des Devis, -Quoted Item Comparison,Comparaison d'Article Soumis, Received Items To Be Billed,Articles Reçus à Facturer, Qty to Order,Quantité à Commander, Requested Items To Be Transferred,Articles Demandés à Transférer, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,L'article référencé Therapy Session overlaps with {0},La session de thérapie chevauche {0}, Therapy Sessions Overlapping,Chevauchement des séances de thérapie, Therapy Plans,Plans de thérapie, +"Item Code, warehouse, quantity are required on row {0}","Le code article, l'entrepôt et la quantité sont obligatoires sur la ligne {0}", +Get Items from Material Requests against this Supplier,Obtenir des articles à partir de demandes d'articles auprès de ce fournisseur, +Enable European Access,Activer l'accès européen, +Creating Purchase Order ...,Création d'une commande d'achat ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Sélectionnez un fournisseur parmi les fournisseurs par défaut des articles ci-dessous. Lors de la sélection, un bon de commande sera effectué contre des articles appartenant uniquement au fournisseur sélectionné.", +Row #{}: You must select {} serial numbers for item {}.,Ligne n ° {}: vous devez sélectionner {} numéros de série pour l'article {}., diff --git a/erpnext/translations/gu.csv b/erpnext/translations/gu.csv index e25c6bba4a..5c2b520d9e 100644 --- a/erpnext/translations/gu.csv +++ b/erpnext/translations/gu.csv @@ -4238,7 +4238,6 @@ Download as JSON,જેસન તરીકે ડાઉનલોડ કરો, End date can not be less than start date,સમાપ્તિ તારીખ પ્રારંભ તારીખ કરતાં ઓછી હોઈ શકતી નથી, For Default Supplier (Optional),ડિફોલ્ટ સપ્લાયર માટે (વૈકલ્પિક), From date cannot be greater than To date,તારીખથી તારીખ કરતાં વધુ હોઈ શકતી નથી, -Get items from,વસ્તુઓ મેળવો, Group by,ગ્રુપ દ્વારા, In stock,ઉપલબ્ધ છે, Item name,વસ્તુ નામ, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ઓર્ડર પ્રવાહો ખરીદી, Purchase Receipt Trends,ખરીદી રસીદ પ્રવાહો, Purchase Register,ખરીદી રજીસ્ટર, Quotation Trends,અવતરણ પ્રવાહો, -Quoted Item Comparison,નોંધાયેલા વસ્તુ સરખામણી, Received Items To Be Billed,પ્રાપ્ત વસ્તુઓ બિલ કરવા, Qty to Order,ઓર્ડર Qty, Requested Items To Be Transferred,વિનંતી વસ્તુઓ ટ્રાન્સફર કરી, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} દ્વાર Therapy Session overlaps with {0},ઉપચાર સત્ર {0 with સાથે ઓવરલેપ થાય છે, Therapy Sessions Overlapping,ઉપચાર સત્રો ઓવરલેપિંગ, Therapy Plans,ઉપચાર યોજનાઓ, +"Item Code, warehouse, quantity are required on row {0}","આઇટમ કોડ, વેરહાઉસ, જથ્થો પંક્તિ પર આવશ્યક છે {0}", +Get Items from Material Requests against this Supplier,આ સપ્લાયર સામે સામગ્રી વિનંતીઓમાંથી આઇટમ્સ મેળવો, +Enable European Access,યુરોપિયન Enableક્સેસને સક્ષમ કરો, +Creating Purchase Order ...,ખરીદી Orderર્ડર બનાવી રહ્યાં છે ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","નીચેની આઇટમ્સના ડિફaultલ્ટ સપ્લાયર્સમાંથી સપ્લાયર પસંદ કરો. પસંદગી પર, ફક્ત પસંદ કરેલા સપ્લાયરની વસ્તુઓ સામે ખરીદ ઓર્ડર આપવામાં આવશે.", +Row #{}: You must select {} serial numbers for item {}.,પંક્તિ # {}: તમારે આઇટમ for for માટે સીરીયલ નંબરો પસંદ કરવા આવશ્યક છે., diff --git a/erpnext/translations/he.csv b/erpnext/translations/he.csv index 43ca52cbe1..29e6f6afc3 100644 --- a/erpnext/translations/he.csv +++ b/erpnext/translations/he.csv @@ -4238,7 +4238,6 @@ Download as JSON,הורד כ- JSON, End date can not be less than start date,תאריך סיום לא יכול להיות פחות מתאריך ההתחלה, For Default Supplier (Optional),לספק ברירת מחדל (אופציונלי), From date cannot be greater than To date,מתאריך לא יכול להיות גדול יותר מאשר תאריך, -Get items from,קבל פריטים מ, Group by,קבוצה על ידי, In stock,במלאי, Item name,שם פריט, @@ -8549,7 +8548,6 @@ Purchase Order Trends,לרכוש מגמות להזמין, Purchase Receipt Trends,מגמות קבלת רכישה, Purchase Register,רכישת הרשמה, Quotation Trends,מגמות ציטוט, -Quoted Item Comparison,פריט מצוטט השוואה, Received Items To Be Billed,פריטים שהתקבלו לחיוב, Qty to Order,כמות להזמנה, Requested Items To Be Transferred,פריטים מבוקשים שיועברו, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,הפריט אליו הוז Therapy Session overlaps with {0},מושב טיפולי חופף עם {0}, Therapy Sessions Overlapping,מפגשי טיפול חופפים, Therapy Plans,תוכניות טיפול, +"Item Code, warehouse, quantity are required on row {0}","קוד פריט, מחסן, כמות נדרשים בשורה {0}", +Get Items from Material Requests against this Supplier,קבל פריטים מבקשות חומר נגד ספק זה, +Enable European Access,אפשר גישה אירופית, +Creating Purchase Order ...,יוצר הזמנת רכש ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","בחר ספק מבין ספקי ברירת המחדל של הפריטים למטה. בבחירה, הזמנת רכש תתבצע כנגד פריטים השייכים לספק שנבחר בלבד.", +Row #{}: You must select {} serial numbers for item {}.,שורה מספר {}: עליך לבחור {} מספרים סידוריים לפריט {}., diff --git a/erpnext/translations/hi.csv b/erpnext/translations/hi.csv index c18532b974..c385fc6ef6 100644 --- a/erpnext/translations/hi.csv +++ b/erpnext/translations/hi.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON के रूप में डाउनलोड करे End date can not be less than start date,समाप्ति तिथि आरंभ तिथि से कम नहीं हो सकता, For Default Supplier (Optional),डिफ़ॉल्ट प्रदायक (वैकल्पिक) के लिए, From date cannot be greater than To date,दिनांक से दिनांक से अधिक नहीं हो सकता है, -Get items from,से आइटम प्राप्त, Group by,समूह द्वारा, In stock,स्टॉक में, Item name,मद का नाम, @@ -8549,7 +8548,6 @@ Purchase Order Trends,आदेश रुझान खरीद, Purchase Receipt Trends,खरीद रसीद रुझान, Purchase Register,इन पंजीकृत खरीद, Quotation Trends,कोटेशन रुझान, -Quoted Item Comparison,उद्धरित मद तुलना, Received Items To Be Billed,बिल करने के लिए प्राप्त आइटम, Qty to Order,मात्रा आदेश को, Requested Items To Be Transferred,हस्तांतरित करने का अनुरोध आइटम, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} द्वार Therapy Session overlaps with {0},थेरेपी सत्र {0} से ओवरलैप होता है, Therapy Sessions Overlapping,थेरेपी सेशन ओवरलैपिंग, Therapy Plans,थेरेपी योजनाएं, +"Item Code, warehouse, quantity are required on row {0}","पंक्ति {0} पर आइटम कोड, गोदाम, मात्रा आवश्यक है", +Get Items from Material Requests against this Supplier,इस आपूर्तिकर्ता के विरुद्ध सामग्री अनुरोध से आइटम प्राप्त करें, +Enable European Access,यूरोपीय पहुँच सक्षम करें, +Creating Purchase Order ...,क्रय आदेश बनाना ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","नीचे दी गई वस्तुओं के डिफ़ॉल्ट आपूर्तिकर्ता से एक आपूर्तिकर्ता का चयन करें। चयन पर, केवल चयनित आपूर्तिकर्ता से संबंधित वस्तुओं के खिलाफ एक खरीद ऑर्डर किया जाएगा।", +Row #{}: You must select {} serial numbers for item {}.,पंक्ति # {}: आपको आइटम {} के लिए {} सीरियल नंबर का चयन करना होगा।, diff --git a/erpnext/translations/hr.csv b/erpnext/translations/hr.csv index aabacdc0af..a544e98868 100644 --- a/erpnext/translations/hr.csv +++ b/erpnext/translations/hr.csv @@ -4238,7 +4238,6 @@ Download as JSON,Preuzmi kao Json, End date can not be less than start date,Datum završetka ne može biti manja od početnog datuma, For Default Supplier (Optional),Za dobavljača zadano (neobavezno), From date cannot be greater than To date,Datum ne može biti veći od datuma, -Get items from,Nabavite stavke iz, Group by,Grupiranje prema, In stock,Na lageru, Item name,Naziv proizvoda, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Trendovi narudžbenica kupnje, Purchase Receipt Trends,Trend primki, Purchase Register,Popis nabave, Quotation Trends,Trend ponuda, -Quoted Item Comparison,Citirano predmeta za usporedbu, Received Items To Be Billed,Primljeni Proizvodi se naplaćuje, Qty to Order,Količina za narudžbu, Requested Items To Be Transferred,Traženi proizvodi spremni za transfer, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Stavka na koju se poziva {0 Therapy Session overlaps with {0},Sjednica terapije preklapa se s {0}, Therapy Sessions Overlapping,Preklapajuće se terapijske sesije, Therapy Plans,Planovi terapije, +"Item Code, warehouse, quantity are required on row {0}","Šifra artikla, skladište, količina potrebni su na retku {0}", +Get Items from Material Requests against this Supplier,Nabavite stavke iz materijalnih zahtjeva protiv ovog dobavljača, +Enable European Access,Omogućiti europski pristup, +Creating Purchase Order ...,Izrada narudžbenice ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Odaberite dobavljača od zadanih dobavljača dolje navedenih stavki. Nakon odabira, narudžbenica će se izvršiti samo za proizvode koji pripadaju odabranom dobavljaču.", +Row #{}: You must select {} serial numbers for item {}.,Redak {{}: Morate odabrati {} serijske brojeve za stavku {}., diff --git a/erpnext/translations/hu.csv b/erpnext/translations/hu.csv index 09acda372e..29f347ecbc 100644 --- a/erpnext/translations/hu.csv +++ b/erpnext/translations/hu.csv @@ -4238,7 +4238,6 @@ Download as JSON,Töltse le JSON néven, End date can not be less than start date,"A befejezés dátuma nem lehet kevesebb, mint a kezdő dátum", For Default Supplier (Optional),Az alapértelmezett beszállító számára (opcionális), From date cannot be greater than To date,"A dátum nem lehet nagyobb, mint a dátum", -Get items from,Tételeket kér le innen, Group by,Csoportosítva, In stock,Raktáron, Item name,Tétel neve, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Beszerzési megrendelések alakulása, Purchase Receipt Trends,Beszerzési nyugták alakulása, Purchase Register,Beszerzési Regisztráció, Quotation Trends,Árajánlatok alakulása, -Quoted Item Comparison,Ajánlott tétel összehasonlítás, Received Items To Be Billed,Számlázandó Beérkezett tételek, Qty to Order,Mennyiség Rendeléshez, Requested Items To Be Transferred,Kérte az átvinni kívánt elemeket, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,A (z) {0} - {1} által hiva Therapy Session overlaps with {0},A terápiás munkamenet átfedésben van a következővel: {0}, Therapy Sessions Overlapping,A terápiás foglalkozások átfedésben vannak, Therapy Plans,Terápiás tervek, +"Item Code, warehouse, quantity are required on row {0}","A (z) {0} sorban tételszám, raktár, mennyiség szükséges", +Get Items from Material Requests against this Supplier,Tételeket szerezhet a szállítóval szembeni anyagi igényekből, +Enable European Access,Engedélyezze az európai hozzáférést, +Creating Purchase Order ...,Megrendelés létrehozása ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Válasszon szállítót az alábbi elemek alapértelmezett szállítói közül. A kiválasztáskor csak a kiválasztott szállítóhoz tartozó termékekre készül megrendelés., +Row #{}: You must select {} serial numbers for item {}.,#. Sor:} {0} sorszámokat kell kiválasztania., diff --git a/erpnext/translations/id.csv b/erpnext/translations/id.csv index 53122ccb71..7175ad2fa7 100644 --- a/erpnext/translations/id.csv +++ b/erpnext/translations/id.csv @@ -4238,7 +4238,6 @@ Download as JSON,Unduh sebagai JSON, End date can not be less than start date,Tanggal Berakhir tidak boleh kurang dari Tanggal Mulai, For Default Supplier (Optional),Untuk Pemasok Default (Opsional), From date cannot be greater than To date,Dari Tanggal tidak dapat lebih besar dari To Date, -Get items from,Mendapatkan Stok Barang-Stok Barang dari, Group by,Kelompok Dengan, In stock,Persediaan, Item name,Nama Item, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Trend Order Pembelian, Purchase Receipt Trends,Tren Nota Penerimaan, Purchase Register,Register Pembelian, Quotation Trends,Trend Penawaran, -Quoted Item Comparison,Perbandingan Produk/Barang yang ditawarkan, Received Items To Be Billed,Produk Diterima Akan Ditagih, Qty to Order,Kuantitas untuk diorder, Requested Items To Be Transferred,Permintaan Produk Akan Ditransfer, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Item yang direferensikan ol Therapy Session overlaps with {0},Sesi Terapi tumpang tindih dengan {0}, Therapy Sessions Overlapping,Sesi Terapi Tumpang Tindih, Therapy Plans,Rencana Terapi, +"Item Code, warehouse, quantity are required on row {0}","Kode Barang, gudang, kuantitas diperlukan di baris {0}", +Get Items from Material Requests against this Supplier,Dapatkan Item dari Permintaan Material terhadap Pemasok ini, +Enable European Access,Aktifkan Akses Eropa, +Creating Purchase Order ...,Membuat Pesanan Pembelian ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Pilih Pemasok dari Pemasok Default item di bawah ini. Saat dipilih, Pesanan Pembelian akan dibuat terhadap barang-barang milik Pemasok terpilih saja.", +Row #{}: You must select {} serial numbers for item {}.,Baris # {}: Anda harus memilih {} nomor seri untuk item {}., diff --git a/erpnext/translations/is.csv b/erpnext/translations/is.csv index 50828baeee..5f56aff3dc 100644 --- a/erpnext/translations/is.csv +++ b/erpnext/translations/is.csv @@ -4238,7 +4238,6 @@ Download as JSON,Sæktu sem JSON, End date can not be less than start date,Lokadagur getur ekki verið minna en upphafsdagur, For Default Supplier (Optional),Fyrir Sjálfgefið Birgir (valfrjálst), From date cannot be greater than To date,Frá Dagsetning má ekki vera meiri en Til Dagsetning, -Get items from,Fá atriði úr, Group by,Hópa eftir, In stock,Á lager, Item name,Item Name, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Purchase Order Trends, Purchase Receipt Trends,Kvittun Trends, Purchase Register,kaup Register, Quotation Trends,Tilvitnun Trends, -Quoted Item Comparison,Vitnað Item Samanburður, Received Items To Be Billed,Móttekin Items verður innheimt, Qty to Order,Magn til að panta, Requested Items To Be Transferred,Umbeðin Items til að flytja, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Atriðið sem {0} - {1} ví Therapy Session overlaps with {0},Meðferðarlotan skarast við {0}, Therapy Sessions Overlapping,Meðferðarlotur skarast, Therapy Plans,Meðferðaráætlanir, +"Item Code, warehouse, quantity are required on row {0}","Vörukóði, vöruhús, magn er krafist í línu {0}", +Get Items from Material Requests against this Supplier,Fáðu hluti frá beiðnum um efni gagnvart þessum birgi, +Enable European Access,Virkja evrópskan aðgang, +Creating Purchase Order ...,Býr til innkaupapöntun ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Veldu birgja frá sjálfgefnum birgjum hlutanna hér að neðan. Við val verður eingöngu gerð innkaupapöntun á hlutum sem tilheyra völdum birgi., +Row #{}: You must select {} serial numbers for item {}.,Röð nr. {}: Þú verður að velja {} raðnúmer fyrir hlut {}., diff --git a/erpnext/translations/it.csv b/erpnext/translations/it.csv index 4da11f1989..3a1d73f344 100644 --- a/erpnext/translations/it.csv +++ b/erpnext/translations/it.csv @@ -4238,7 +4238,6 @@ Download as JSON,Scarica come JSON, End date can not be less than start date,Data di Fine non può essere inferiore a Data di inizio, For Default Supplier (Optional),Per fornitore predefinito (facoltativo), From date cannot be greater than To date,Dalla data non può essere maggiore di Alla data, -Get items from,Ottenere elementi dal, Group by,Raggruppa per, In stock,disponibile, Item name,Nome Articolo, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Acquisto Tendenze Ordine, Purchase Receipt Trends,Acquisto Tendenze Receipt, Purchase Register,Registro Acquisti, Quotation Trends,Tendenze di preventivo, -Quoted Item Comparison,Articolo Citato Confronto, Received Items To Be Billed,Oggetti ricevuti da fatturare, Qty to Order,Qtà da Ordinare, Requested Items To Be Transferred,Voci si chiede il trasferimento, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,L'articolo a cui fa rif Therapy Session overlaps with {0},La sessione di terapia si sovrappone a {0}, Therapy Sessions Overlapping,Sessioni di terapia sovrapposte, Therapy Plans,Piani terapeutici, +"Item Code, warehouse, quantity are required on row {0}","Codice articolo, magazzino e quantità sono obbligatori nella riga {0}", +Get Items from Material Requests against this Supplier,Ottieni articoli da richieste di materiale contro questo fornitore, +Enable European Access,Consentire l'accesso europeo, +Creating Purchase Order ...,Creazione dell'ordine di acquisto ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Seleziona un fornitore dai fornitori predefiniti degli articoli seguenti. Alla selezione, verrà effettuato un Ordine di acquisto solo per gli articoli appartenenti al Fornitore selezionato.", +Row #{}: You must select {} serial numbers for item {}.,Riga # {}: è necessario selezionare i {} numeri di serie per l'articolo {}., diff --git a/erpnext/translations/ja.csv b/erpnext/translations/ja.csv index 3fa1c18a48..6e2eaae4a4 100644 --- a/erpnext/translations/ja.csv +++ b/erpnext/translations/ja.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSONとしてダウンロード, End date can not be less than start date,終了日は開始日より短くすることはできません, For Default Supplier (Optional),デフォルトサプライヤ(オプション), From date cannot be greater than To date,開始日を終了日より大きくすることはできません, -Get items from,アイテム取得元, Group by,グループ化, In stock,在庫あり, Item name,アイテム名, @@ -8549,7 +8548,6 @@ Purchase Order Trends,発注傾向, Purchase Receipt Trends,領収書傾向, Purchase Register,仕入帳, Quotation Trends,見積傾向, -Quoted Item Comparison,引用符で囲まれた項目の比較, Received Items To Be Billed,支払予定受領アイテム, Qty to Order,注文数, Requested Items To Be Transferred,移転予定の要求アイテム, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0}-{1}で参照されて Therapy Session overlaps with {0},セラピーセッションが{0}と重複しています, Therapy Sessions Overlapping,重複する治療セッション, Therapy Plans,治療計画, +"Item Code, warehouse, quantity are required on row {0}",行{0}にはアイテムコード、倉庫、数量が必要です, +Get Items from Material Requests against this Supplier,このサプライヤーに対する重要な要求からアイテムを取得する, +Enable European Access,ヨーロッパへのアクセスを有効にする, +Creating Purchase Order ...,注文書の作成..。, +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",以下の項目のデフォルトサプライヤーからサプライヤーを選択します。選択時に、選択したサプライヤーに属するアイテムに対してのみ発注書が作成されます。, +Row #{}: You must select {} serial numbers for item {}.,行#{}:アイテム{}の{}シリアル番号を選択する必要があります。, diff --git a/erpnext/translations/km.csv b/erpnext/translations/km.csv index e1a01b018c..e2a528cea2 100644 --- a/erpnext/translations/km.csv +++ b/erpnext/translations/km.csv @@ -4238,7 +4238,6 @@ Download as JSON,ទាញយកជា Json ។, End date can not be less than start date,កាលបរិច្ឆេទបញ្ចប់មិនអាចតិចជាងកាលបរិច្ឆេទចាប់ផ្ដើមទេ, For Default Supplier (Optional),សម្រាប់អ្នកផ្គត់ផ្គង់លំនាំដើម (ស្រេចចិត្ត), From date cannot be greater than To date,ពីកាលបរិច្ឆេទមិនអាចមានចំនួនច្រើនជាងកាលបរិច្ឆេទ, -Get items from,ទទួលបានមុខទំនិញពី, Group by,ក្រុមតាម, In stock,នៅក្នុងស្តុក, Item name,ឈ្មោះធាតុ, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ទិញលំដាប់និន្នាការ, Purchase Receipt Trends,និន្នាការបង្កាន់ដៃទិញ, Purchase Register,ទិញចុះឈ្មោះ, Quotation Trends,សម្រង់និន្នាការ, -Quoted Item Comparison,ធាតុដកស្រង់សម្តីប្រៀបធៀប, Received Items To Be Billed,ទទួលបានធាតុដែលនឹងត្រូវបានផ្សព្វផ្សាយ, Qty to Order,qty ម៉ង់ទិញ, Requested Items To Be Transferred,ធាតុដែលបានស្នើសុំឱ្យគេបញ្ជូន, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,ធាតុដែលយោ Therapy Session overlaps with {0},វគ្គនៃការព្យាបាលត្រួតគ្នាជាមួយ {0}, Therapy Sessions Overlapping,ការព្យាបាលដោយការត្រួតគ្នា, Therapy Plans,ផែនការព្យាបាល, +"Item Code, warehouse, quantity are required on row {0}",លេខកូដទំនិញឃ្លាំងបរិមាណត្រូវបានទាមទារនៅជួរ {0}, +Get Items from Material Requests against this Supplier,ទទួលបានវត្ថុពីសំណើសម្ភារៈប្រឆាំងនឹងអ្នកផ្គត់ផ្គង់នេះ, +Enable European Access,បើកដំណើរការចូលអឺរ៉ុប, +Creating Purchase Order ...,កំពុងបង្កើតការបញ្ជាទិញ ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",ជ្រើសរើសអ្នកផ្គត់ផ្គង់ពីអ្នកផ្គត់ផ្គង់លំនាំដើមនៃធាតុខាងក្រោម។ នៅពេលជ្រើសរើសការបញ្ជាទិញនឹងត្រូវធ្វើឡើងប្រឆាំងនឹងរបស់របរដែលជាកម្មសិទ្ធិរបស់អ្នកផ្គត់ផ្គង់ដែលបានជ្រើសរើសតែប៉ុណ្ណោះ។, +Row #{}: You must select {} serial numbers for item {}.,ជួរទី # {}៖ អ្នកត្រូវតែជ្រើសរើស {} លេខស៊េរីសម្រាប់ធាតុ {} ។, diff --git a/erpnext/translations/kn.csv b/erpnext/translations/kn.csv index fdd798dbfc..4a9173d4a0 100644 --- a/erpnext/translations/kn.csv +++ b/erpnext/translations/kn.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON ಆಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ, End date can not be less than start date,ಅಂತಿಮ ದಿನಾಂಕ ಪ್ರಾರಂಭ ದಿನಾಂಕಕ್ಕಿಂತ ಕಡಿಮೆ ಇರುವಂತಿಲ್ಲ, For Default Supplier (Optional),ಡೀಫಾಲ್ಟ್ ಪೂರೈಕೆದಾರರಿಗಾಗಿ (ಐಚ್ಛಿಕ), From date cannot be greater than To date,ದಿನಾಂಕದಿಂದ ದಿನಾಂಕಕ್ಕಿಂತಲೂ ಹೆಚ್ಚಿನದಾಗಿರುವಂತಿಲ್ಲ, -Get items from,ಐಟಂಗಳನ್ನು ಪಡೆಯಿರಿ, Group by,ಗುಂಪಿನ, In stock,ಉಪಲಬ್ದವಿದೆ, Item name,ಐಟಂ ಹೆಸರು, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ಆರ್ಡರ್ ಟ್ರೆಂಡ್ಸ್ ಖರಿ Purchase Receipt Trends,ಖರೀದಿ ರಸೀತಿ ಟ್ರೆಂಡ್ಸ್, Purchase Register,ಖರೀದಿ ನೋಂದಣಿ, Quotation Trends,ನುಡಿಮುತ್ತುಗಳು ಟ್ರೆಂಡ್ಸ್, -Quoted Item Comparison,ಉಲ್ಲೇಖಿಸಿದ ಐಟಂ ಹೋಲಿಕೆ, Received Items To Be Billed,ಪಾವತಿಸಬೇಕಾಗುತ್ತದೆ ಸ್ವೀಕರಿಸಿದ ಐಟಂಗಳು, Qty to Order,ಪ್ರಮಾಣ ಆರ್ಡರ್, Requested Items To Be Transferred,ಬದಲಾಯಿಸಿಕೊಳ್ಳುವಂತೆ ವಿನಂತಿಸಲಾಗಿದೆ ಐಟಂಗಳು, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1 by ನಿಂದ Therapy Session overlaps with {0},ಥೆರಪಿ ಸೆಷನ್ {0 with ನೊಂದಿಗೆ ಅತಿಕ್ರಮಿಸುತ್ತದೆ, Therapy Sessions Overlapping,ಥೆರಪಿ ಸೆಷನ್‌ಗಳು ಅತಿಕ್ರಮಿಸುತ್ತವೆ, Therapy Plans,ಚಿಕಿತ್ಸೆಯ ಯೋಜನೆಗಳು, +"Item Code, warehouse, quantity are required on row {0}","Code 0 row ಸಾಲಿನಲ್ಲಿ ಐಟಂ ಕೋಡ್, ಗೋದಾಮು, ಪ್ರಮಾಣ ಅಗತ್ಯವಿದೆ", +Get Items from Material Requests against this Supplier,ಈ ಸರಬರಾಜುದಾರರ ವಿರುದ್ಧ ವಸ್ತು ವಿನಂತಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ಪಡೆಯಿರಿ, +Enable European Access,ಯುರೋಪಿಯನ್ ಪ್ರವೇಶವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ, +Creating Purchase Order ...,ಖರೀದಿ ಆದೇಶವನ್ನು ರಚಿಸಲಾಗುತ್ತಿದೆ ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","ಕೆಳಗಿನ ಐಟಂಗಳ ಡೀಫಾಲ್ಟ್ ಪೂರೈಕೆದಾರರಿಂದ ಸರಬರಾಜುದಾರರನ್ನು ಆಯ್ಕೆಮಾಡಿ. ಆಯ್ಕೆಯ ಮೇಲೆ, ಆಯ್ದ ಸರಬರಾಜುದಾರರಿಗೆ ಮಾತ್ರ ಸೇರಿದ ವಸ್ತುಗಳ ವಿರುದ್ಧ ಖರೀದಿ ಆದೇಶವನ್ನು ಮಾಡಲಾಗುತ್ತದೆ.", +Row #{}: You must select {} serial numbers for item {}.,ಸಾಲು # {}: ನೀವು item item ಐಟಂಗೆ {} ಸರಣಿ ಸಂಖ್ಯೆಗಳನ್ನು ಆರಿಸಬೇಕು., diff --git a/erpnext/translations/ko.csv b/erpnext/translations/ko.csv index 1ba119cdb9..c051b07bea 100644 --- a/erpnext/translations/ko.csv +++ b/erpnext/translations/ko.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON으로 다운로드, End date can not be less than start date,종료일은 시작일보다 짧을 수 없습니다., For Default Supplier (Optional),기본 공급 업체 (선택 사항), From date cannot be greater than To date,날짜에서 날짜보다 클 수 없습니다, -Get items from,에서 항목을 가져 오기, Group by,그룹으로, In stock,재고, Item name,품명, @@ -8549,7 +8548,6 @@ Purchase Order Trends,주문 동향을 구매, Purchase Receipt Trends,구매 영수증 동향, Purchase Register,회원에게 구매, Quotation Trends,견적 동향, -Quoted Item Comparison,인용 상품 비교, Received Items To Be Billed,청구에 주어진 항목, Qty to Order,수량은 주문, Requested Items To Be Transferred,전송할 요청 항목, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0}-{1}에서 참조하는 Therapy Session overlaps with {0},치료 세션이 {0}와 (과) 겹칩니다., Therapy Sessions Overlapping,겹치는 치료 세션, Therapy Plans,치료 계획, +"Item Code, warehouse, quantity are required on row {0}","{0} 행에 품목 코드, 창고, 수량이 필요합니다.", +Get Items from Material Requests against this Supplier,이 공급자에 대한 자재 요청에서 품목 가져 오기, +Enable European Access,유럽 액세스 활성화, +Creating Purchase Order ...,구매 오더 생성 ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",아래 항목의 기본 공급자에서 공급자를 선택합니다. 선택시 선택한 공급 업체에 속한 품목에 대해서만 구매 주문이 작성됩니다., +Row #{}: You must select {} serial numbers for item {}.,행 # {} : 항목 {}에 대해 {} 일련 번호를 선택해야합니다., diff --git a/erpnext/translations/ku.csv b/erpnext/translations/ku.csv index 5d9d472071..6962ea1ef1 100644 --- a/erpnext/translations/ku.csv +++ b/erpnext/translations/ku.csv @@ -4238,7 +4238,6 @@ Download as JSON,Wekî JSON dakêşin, End date can not be less than start date,Dîrok Dîroka Destpêk Destpêk Dibe, For Default Supplier (Optional),Ji bo Default Supplier (alternatîf), From date cannot be greater than To date,Ji Date ne dikarin bibin mezintir To Date, -Get items from,Get tomar ji, Group by,Koma By, In stock,Ez bêzarim, Item name,Navê Navekî, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Bikirin Order Trends, Purchase Receipt Trends,Trends kirînê Meqbûz, Purchase Register,Buy Register, Quotation Trends,Trends quotation, -Quoted Item Comparison,Babetê têbinî eyna, Received Items To Be Billed,Pêşwaziya Nawy ye- Be, Qty to Order,Qty siparîş, Requested Items To Be Transferred,Nawy xwestin veguhestin, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Tişta ku ji hêla {0} - {1 Therapy Session overlaps with {0},Danişîna Terapiyê bi {0} re li hevûdu dike, Therapy Sessions Overlapping,Danişînên Terapiyê Li Hev Dikevin, Therapy Plans,Planên Terapiyê, +"Item Code, warehouse, quantity are required on row {0}","Koda tiştê, embarê, hejmar li ser rêzê hewce ne {0}", +Get Items from Material Requests against this Supplier,Li dijî vê Pêşkêşkerê Tiştan ji Daxwazên Maddî bistînin, +Enable European Access,Destûra Ewropî çalak bikin, +Creating Purchase Order ...,Afirandina Biryara Kirînê ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Ji Pargîdaniyên Pêşwext ên jêrîn ve Pêşkêşvanek hilbijêrin. Li ser hilbijartinê, dê Biryarnameyek Kirînê li dijî tiştên ku tenê ji Pêşkêşkarê hilbijartî ne pêk were.", +Row #{}: You must select {} serial numbers for item {}.,Rêzok # {}: Divê hûn {} hejmarên rêzê ji bo hêmanê {} hilbijêrin., diff --git a/erpnext/translations/lo.csv b/erpnext/translations/lo.csv index 2220ac3c17..b61476cf17 100644 --- a/erpnext/translations/lo.csv +++ b/erpnext/translations/lo.csv @@ -4238,7 +4238,6 @@ Download as JSON,ດາວໂຫລດເປັນ JSON, End date can not be less than start date,ວັນສິ້ນສຸດບໍ່ສາມາດນ້ອຍກວ່າວັນເລີ່ມຕົ້ນ, For Default Supplier (Optional),ສໍາລັບຜູ້ໃຫ້ບໍລິການມາດຕະຖານ (ທາງເລືອກ), From date cannot be greater than To date,ຈາກວັນທີບໍ່ສາມາດຈະມີຫຼາຍຂຶ້ນກ່ວາເຖິງວັນທີ່, -Get items from,ໄດ້ຮັບການລາຍການຈາກ, Group by,ກຸ່ມໂດຍ, In stock,ໃນສາງ, Item name,ຊື່ສິນຄ້າ, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ຊື້ແນວໂນ້ມຄໍາສັ່ງ, Purchase Receipt Trends,ແນວໂນ້ມການຊື້ຮັບ, Purchase Register,ລົງທະບຽນການຊື້, Quotation Trends,ແນວໂນ້ມວົງຢືມ, -Quoted Item Comparison,ປຽບທຽບບາຍດີທຸກທ່ານ Item, Received Items To Be Billed,ລາຍການທີ່ໄດ້ຮັບການໄດ້ຮັບການ billed, Qty to Order,ຈໍານວນທີ່ຈະສັ່ງຊື້ສິນຄ້າ, Requested Items To Be Transferred,ການຮ້ອງຂໍໃຫ້ໄດ້ຮັບການໂອນ, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,ລາຍການທີ່ Therapy Session overlaps with {0},Therapy Session ຊ້ ຳ ກັບ {0}, Therapy Sessions Overlapping,ການປິ່ນປົວດ້ວຍການຊໍ້າຊ້ອນ, Therapy Plans,ແຜນການປິ່ນປົວ, +"Item Code, warehouse, quantity are required on row {0}","ລະຫັດສິນຄ້າ, ຄັງສິນຄ້າ, ຈຳ ນວນທີ່ຕ້ອງການຢູ່ແຖວ {0}", +Get Items from Material Requests against this Supplier,ໄດ້ຮັບສິນຄ້າຈາກຂໍ້ຮຽກຮ້ອງດ້ານວັດຖຸຕໍ່ຜູ້ສະ ໜອງ ນີ້, +Enable European Access,ເປີດໃຊ້ງານ European Access, +Creating Purchase Order ...,ການສ້າງໃບສັ່ງຊື້ ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","ເລືອກຜູ້ສະ ໜອງ ສິນຄ້າຈາກຜູ້ສະ ໜອງ ສິນຄ້າເລີ່ມຕົ້ນຂອງລາຍການຂ້າງລຸ່ມນີ້. ໃນການເລືອກ, ຄຳ ສັ່ງຊື້ສິນຄ້າຈະຖືກຕໍ່ຕ້ານກັບສິນຄ້າທີ່ເປັນຂອງຜູ້ສະ ໜອງ ທີ່ຖືກຄັດເລືອກເທົ່ານັ້ນ.", +Row #{}: You must select {} serial numbers for item {}.,ແຖວ # {}: ທ່ານຕ້ອງເລືອກ {} ເລກ ລຳ ດັບ ສຳ ລັບລາຍການ {}., diff --git a/erpnext/translations/lt.csv b/erpnext/translations/lt.csv index e98f2f2143..78571f9624 100644 --- a/erpnext/translations/lt.csv +++ b/erpnext/translations/lt.csv @@ -4238,7 +4238,6 @@ Download as JSON,Atsisiųsti kaip JSON, End date can not be less than start date,Galutinė data negali būti mažesnė už pradžios datą, For Default Supplier (Optional),Numatytam tiekėjui (neprivaloma), From date cannot be greater than To date,Nuo datos negali būti didesnis nei iki datos, -Get items from,Gauk elementus iš, Group by,Grupuoti pagal, In stock,Sandelyje, Item name,Daikto pavadinimas, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Pirkimui užsakyti tendencijos, Purchase Receipt Trends,Pirkimo kvito tendencijos, Purchase Register,pirkimo Registruotis, Quotation Trends,Kainų tendencijos, -Quoted Item Comparison,Cituojamas punktas Palyginimas, Received Items To Be Billed,Gauti duomenys turi būti apmokestinama, Qty to Order,Kiekis užsisakyti, Requested Items To Be Transferred,Pageidaujami daiktai turi būti perkeltos, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Elementas, nurodytas {0} - Therapy Session overlaps with {0},Terapijos seansas sutampa su {0}, Therapy Sessions Overlapping,Terapijos seansai sutampa, Therapy Plans,Terapijos planai, +"Item Code, warehouse, quantity are required on row {0}","Prekės kodas, sandėlis, kiekis būtini {0} eilutėje", +Get Items from Material Requests against this Supplier,Gaukite elementus iš šio tiekėjo materialinių užklausų, +Enable European Access,Įgalinti Europos prieigą, +Creating Purchase Order ...,Kuriamas pirkimo užsakymas ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Pasirinkite tiekėją iš toliau nurodytų gaminių iš numatytųjų tiekėjų. Pasirinkus pirkimą, bus sudarytos tik prekės, priklausančios pasirinktam tiekėjui.", +Row #{}: You must select {} serial numbers for item {}.,# Eilutė {}: turite pasirinkti {} prekės serijos numerius {}., diff --git a/erpnext/translations/lv.csv b/erpnext/translations/lv.csv index 5fdedc8fc7..cbf04855d0 100644 --- a/erpnext/translations/lv.csv +++ b/erpnext/translations/lv.csv @@ -4238,7 +4238,6 @@ Download as JSON,Lejupielādēt kā JSON, End date can not be less than start date,Beigu Datums nevar būt mazāks par sākuma datuma, For Default Supplier (Optional),Paredzētajam piegādātājam (neobligāti), From date cannot be greater than To date,No datuma nevar būt lielāka par datumu, -Get items from,Dabūtu preces no, Group by,Group By, In stock,Noliktavā, Item name,Vienības nosaukums, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Pirkuma pasūtījuma tendences, Purchase Receipt Trends,Pirkuma čeka tendences, Purchase Register,Pirkuma Reģistrēties, Quotation Trends,Piedāvājumu tendences, -Quoted Item Comparison,Citēts Prece salīdzinājums, Received Items To Be Billed,Saņemtie posteņi ir Jāmaksā, Qty to Order,Daudz pasūtījuma, Requested Items To Be Transferred,Pieprasīto pozīcijas jāpārskaita, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Vienumam, uz kuru atsaucas Therapy Session overlaps with {0},Terapijas sesija pārklājas ar {0}, Therapy Sessions Overlapping,Terapijas sesijas pārklājas, Therapy Plans,Terapijas plāni, +"Item Code, warehouse, quantity are required on row {0}","Rindā {0} ir nepieciešams preces kods, noliktava un daudzums", +Get Items from Material Requests against this Supplier,Iegūstiet preces no materiāliem pieprasījumiem pret šo piegādātāju, +Enable European Access,Iespējot Eiropas piekļuvi, +Creating Purchase Order ...,Notiek pirkuma pasūtījuma izveide ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Izvēlieties piegādātāju no tālāk norādīto vienumu noklusējuma piegādātājiem. Pēc izvēles tiks veikts pirkuma pasūtījums tikai par precēm, kas pieder izvēlētajam piegādātājam.", +Row #{}: You must select {} serial numbers for item {}.,#. Rinda: jums jāizvēlas {} vienuma sērijas numuri {}., diff --git a/erpnext/translations/mk.csv b/erpnext/translations/mk.csv index fa3de63675..7008025534 100644 --- a/erpnext/translations/mk.csv +++ b/erpnext/translations/mk.csv @@ -4238,7 +4238,6 @@ Download as JSON,Преземете како JSON, End date can not be less than start date,Крајниот датум не може да биде помал од датумот на почеток, For Default Supplier (Optional),За стандарден добавувач (опционално), From date cannot be greater than To date,Од датумот не може да биде поголем од датум, -Get items from,Се предмети од, Group by,Со група, In stock,На залиха, Item name,Точка Име, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Нарачка трендови, Purchase Receipt Trends,Купување Потврда трендови, Purchase Register,Купување Регистрирај се, Quotation Trends,Трендови на Понуди, -Quoted Item Comparison,Цитирано Точка споредба, Received Items To Be Billed,Примените предмети да бидат фактурирани, Qty to Order,Количина да нарачате, Requested Items To Be Transferred,Бара предмети да бидат префрлени, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Ставката на ко Therapy Session overlaps with {0},Сесијата за терапија се преклопува со {0}, Therapy Sessions Overlapping,Сесии за терапија што се преклопуваат, Therapy Plans,Планови за терапија, +"Item Code, warehouse, quantity are required on row {0}","Кодот на објектот, складиштето, количината се потребни на редот {0}", +Get Items from Material Requests against this Supplier,Набавете предмети од материјални барања против овој добавувач, +Enable European Access,Овозможете европски пристап, +Creating Purchase Order ...,Создавање налог за набавка ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Изберете снабдувач од Стандардните добавувачи на ставките подолу. При избор, ќе се изврши Нарачка за набавка само на предмети што припаѓаат на избраниот Добавувач.", +Row #{}: You must select {} serial numbers for item {}.,Ред # {}: Мора да изберете {} сериски броеви за ставка {}., diff --git a/erpnext/translations/ml.csv b/erpnext/translations/ml.csv index 6c9bcfc655..f917969345 100644 --- a/erpnext/translations/ml.csv +++ b/erpnext/translations/ml.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON ആയി ഡൗൺലോഡുചെയ്യുക, End date can not be less than start date,അവസാന തീയതി ആരംഭ തീയതിക്ക് കുറവായിരിക്കരുത്, For Default Supplier (Optional),സ്ഥിര വിതരണക്കാരന് (ഓപ്ഷണൽ), From date cannot be greater than To date,തീയതി മുതൽ തീയതി വരെ വലിയതായിരിക്കരുത്, -Get items from,നിന്ന് ഇനങ്ങൾ നേടുക, Group by,ഗ്രൂപ്പ്, In stock,സ്റ്റോക്കുണ്ട്, Item name,ഇനം പേര്, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ഓർഡർ ട്രെൻഡുകൾ വാങ്ങ Purchase Receipt Trends,വാങ്ങൽ രസീത് ട്രെൻഡുകൾ, Purchase Register,രജിസ്റ്റർ വാങ്ങുക, Quotation Trends,ക്വട്ടേഷൻ ട്രെൻഡുകൾ, -Quoted Item Comparison,ഉദ്ധരിച്ച ഇനം താരതമ്യം, Received Items To Be Billed,ബില്ല് ലഭിച്ച ഇനങ്ങൾ, Qty to Order,ഓർഡർ Qty, Requested Items To Be Transferred,മാറ്റിയത് അഭ്യർത്ഥിച്ചു ഇനങ്ങൾ, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1 by പരാമർ Therapy Session overlaps with {0},തെറാപ്പി സെഷൻ {0 with ഓവർലാപ്പ് ചെയ്യുന്നു, Therapy Sessions Overlapping,തെറാപ്പി സെഷനുകൾ ഓവർലാപ്പുചെയ്യുന്നു, Therapy Plans,തെറാപ്പി പദ്ധതികൾ, +"Item Code, warehouse, quantity are required on row {0}","Code 0 row വരിയിൽ ഇനം കോഡ്, വെയർഹ house സ്, അളവ് ആവശ്യമാണ്", +Get Items from Material Requests against this Supplier,ഈ വിതരണക്കാരനെതിരായ മെറ്റീരിയൽ അഭ്യർത്ഥനകളിൽ നിന്ന് ഇനങ്ങൾ നേടുക, +Enable European Access,യൂറോപ്യൻ ആക്സസ് പ്രാപ്തമാക്കുക, +Creating Purchase Order ...,വാങ്ങൽ ഓർഡർ സൃഷ്ടിക്കുന്നു ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","ചുവടെയുള്ള ഇനങ്ങളുടെ സ്ഥിരസ്ഥിതി വിതരണക്കാരിൽ നിന്ന് ഒരു വിതരണക്കാരനെ തിരഞ്ഞെടുക്കുക. തിരഞ്ഞെടുക്കുമ്പോൾ, തിരഞ്ഞെടുത്ത വിതരണക്കാരന്റെ മാത്രം ഇനങ്ങൾക്കെതിരെ ഒരു വാങ്ങൽ ഓർഡർ നൽകും.", +Row #{}: You must select {} serial numbers for item {}.,വരി # {}: നിങ്ങൾ item item ഇനത്തിനായി {} സീരിയൽ നമ്പറുകൾ തിരഞ്ഞെടുക്കണം., diff --git a/erpnext/translations/mr.csv b/erpnext/translations/mr.csv index 1d63caf508..9c41ce6f33 100644 --- a/erpnext/translations/mr.csv +++ b/erpnext/translations/mr.csv @@ -4238,7 +4238,6 @@ Download as JSON,जेसन म्हणून डाउनलोड करा End date can not be less than start date,समाप्ती तारीख प्रारंभ तारखेच्या पेक्षा कमी असू शकत नाही, For Default Supplier (Optional),डीफॉल्ट सप्लायर (वैकल्पिक) साठी, From date cannot be greater than To date,तारखेपासून तारखेपेक्षा जास्त असू शकत नाही, -Get items from,आयटम मिळवा, Group by,गट, In stock,स्टॉक मध्ये, Item name,आयटम नाव, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ऑर्डर ट्रेन्ड खरेदी, Purchase Receipt Trends,खरेदी पावती ट्रेन्ड, Purchase Register,खरेदी नोंदणी, Quotation Trends,कोटेशन ट्रेन्ड, -Quoted Item Comparison,उद्धृत बाबींचा तुलना, Received Items To Be Billed,बिल करायचे प्राप्त आयटम, Qty to Order,मागणी करण्यासाठी Qty, Requested Items To Be Transferred,विनंती आयटम हस्तांतरित करणे, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} द्वार Therapy Session overlaps with {0},थेरपी सत्र {0 with सह आच्छादित होते, Therapy Sessions Overlapping,थेरपी सत्रे आच्छादित, Therapy Plans,थेरपी योजना, +"Item Code, warehouse, quantity are required on row {0}","पंक्तीवर आयटम कोड, कोठार, प्रमाण आवश्यक आहे {0}", +Get Items from Material Requests against this Supplier,या पुरवठादाराविरूद्ध मटेरियल रिक्वेस्टचे आयटम मिळवा, +Enable European Access,युरोपियन प्रवेश सक्षम करा, +Creating Purchase Order ...,खरेदी ऑर्डर तयार करत आहे ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","खालील बाबींच्या डीफॉल्ट पुरवठादाराकडील पुरवठादार निवडा. निवडीवर, केवळ निवडलेल्या पुरवठादाराच्या वस्तूंच्या विरुद्ध खरेदी ऑर्डर देण्यात येईल.", +Row #{}: You must select {} serial numbers for item {}.,पंक्ती # {}: आपण आयटम for for साठी} numbers अनुक्रमांक निवडणे आवश्यक आहे., diff --git a/erpnext/translations/ms.csv b/erpnext/translations/ms.csv index 1d9aa75987..1483844990 100644 --- a/erpnext/translations/ms.csv +++ b/erpnext/translations/ms.csv @@ -4238,7 +4238,6 @@ Download as JSON,Muat turun sebagai JSON, End date can not be less than start date,Tarikh akhir tidak boleh kurang daripada tarikh mula, For Default Supplier (Optional),Untuk pembekal lalai (pilihan), From date cannot be greater than To date,Dari Tarikh tidak boleh lebih besar daripada Dating, -Get items from,Mendapatkan barangan dari, Group by,Group By, In stock,Dalam stok, Item name,Nama Item, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Membeli Trend Pesanan, Purchase Receipt Trends,Trend Resit Pembelian, Purchase Register,Pembelian Daftar, Quotation Trends,Trend Sebut Harga, -Quoted Item Comparison,Perkara dipetik Perbandingan, Received Items To Be Billed,Barangan yang diterima dikenakan caj, Qty to Order,Qty Aturan, Requested Items To Be Transferred,Item yang diminta Akan Dipindahkan, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Item yang dirujuk oleh {0} Therapy Session overlaps with {0},Sesi Terapi bertindih dengan {0}, Therapy Sessions Overlapping,Sesi Terapi Bertindih, Therapy Plans,Rancangan Terapi, +"Item Code, warehouse, quantity are required on row {0}","Kod Item, gudang, kuantiti diperlukan pada baris {0}", +Get Items from Material Requests against this Supplier,Dapatkan Item dari Permintaan Bahan terhadap Pembekal ini, +Enable European Access,Dayakan Akses Eropah, +Creating Purchase Order ...,Membuat Pesanan Pembelian ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Pilih Pembekal dari Pembekal Lalai item di bawah. Pada pilihan, Pesanan Pembelian akan dibuat terhadap barang-barang milik Pembekal terpilih sahaja.", +Row #{}: You must select {} serial numbers for item {}.,Baris # {}: Anda mesti memilih {} nombor siri untuk item {}., diff --git a/erpnext/translations/my.csv b/erpnext/translations/my.csv index a505a5ccb5..d15ec1ec71 100644 --- a/erpnext/translations/my.csv +++ b/erpnext/translations/my.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON အဖြစ် Download, End date can not be less than start date,အဆုံးနေ့စွဲ Start ကိုနေ့စွဲထက်လျော့နည်းမဖွစျနိုငျ, For Default Supplier (Optional),ပုံမှန်ပေးသွင်း (ရွေးချယ်နိုင်), From date cannot be greater than To date,နေ့စွဲကနေနေ့စွဲရန်ထက် သာ. ကြီးမြတ်မဖွစျနိုငျ, -Get items from,အထဲကပစ္စည်းတွေကို Get, Group by,Group မှဖြင့်, In stock,ကုန်ပစ္စည်းလက်ဝယ်ရှိ, Item name,item အမည်, @@ -8549,7 +8548,6 @@ Purchase Order Trends,အမိန့်ခေတ်ရေစီးကြော Purchase Receipt Trends,ဝယ်ယူခြင်းပြေစာခေတ်ရေစီးကြောင်း, Purchase Register,မှတ်ပုံတင်မည်ဝယ်ယူ, Quotation Trends,စျေးနှုန်းခေတ်ရေစီးကြောင်း, -Quoted Item Comparison,ကိုးကားအရာဝတ္ထုနှိုင်းယှဉ်ခြင်း, Received Items To Be Billed,ကြေညာတဲ့ခံရဖို့ရရှိထားသည့်ပစ္စည်းများ, Qty to Order,ရမလဲမှ Qty, Requested Items To Be Transferred,လွှဲပြောင်းရန်မေတ္တာရပ်ခံပစ္စည်းများ, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} ကညွှန Therapy Session overlaps with {0},ကုထုံးတွေ့ဆုံဆွေးနွေးမှု {0} နှင့်ထပ်နေသည်, Therapy Sessions Overlapping,ကုထုံးတွေ့ဆုံဆွေးနွေးပွဲ, Therapy Plans,ကုထုံးအစီအစဉ်များ, +"Item Code, warehouse, quantity are required on row {0}","item ကုဒ်, ဂိုဒေါင်, အရေအတွက်အတန်း {0} တွင်လိုအပ်သည်။", +Get Items from Material Requests against this Supplier,ဒီပေးသွင်းသူဆန့်ကျင်ပစ္စည်းတောင်းဆိုမှုများမှပစ္စည်းများရယူပါ, +Enable European Access,ဥရောပ Access ကိုဖွင့်, +Creating Purchase Order ...,ဝယ်ယူမှုအမိန့်ကိုဖန်တီးခြင်း ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",အောက်ဖော်ပြပါပစ္စည်းများ၏ပုံမှန်ပေးသွင်းသူထံမှပေးသွင်းရွေးချယ်ပါ။ ရွေးချယ်မှုတွင်ရွေးချယ်ထားသောပေးသွင်းသူနှင့်သာသက်ဆိုင်သောပစ္စည်းများကိုသာ ၀ ယ်ရန်အမိန့်ပေးလိမ့်မည်။, +Row #{}: You must select {} serial numbers for item {}.,Row # {} - item {} အတွက်နံပါတ်စဉ်ဆက်ရွေးပါ။, diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv index 33b1098e17..fbadc02327 100644 --- a/erpnext/translations/nl.csv +++ b/erpnext/translations/nl.csv @@ -4238,7 +4238,6 @@ Download as JSON,Downloaden als JSON, End date can not be less than start date,Einddatum kan niet vroeger zijn dan startdatum, For Default Supplier (Optional),Voor standaardleverancier (optioneel), From date cannot be greater than To date,Vanaf de datum kan niet groter zijn dan tot nu toe, -Get items from,Krijgen items uit, Group by,Groeperen volgens, In stock,Op voorraad, Item name,Artikelnaam, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Inkooporder Trends, Purchase Receipt Trends,Ontvangstbevestiging Trends, Purchase Register,Inkoop Register, Quotation Trends,Offerte Trends, -Quoted Item Comparison,Geciteerd Item Vergelijking, Received Items To Be Billed,Ontvangen artikelen nog te factureren, Qty to Order,Aantal te bestellen, Requested Items To Be Transferred,Aangevraagde Artikelen te Verplaatsen, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Het artikel waarnaar wordt Therapy Session overlaps with {0},Therapiesessie overlapt met {0}, Therapy Sessions Overlapping,Therapiesessies overlappen elkaar, Therapy Plans,Therapieplannen, +"Item Code, warehouse, quantity are required on row {0}","Artikelcode, magazijn, aantal zijn vereist op rij {0}", +Get Items from Material Requests against this Supplier,Artikelen ophalen van materiaalverzoeken tegen deze leverancier, +Enable European Access,Schakel Europese toegang in, +Creating Purchase Order ...,Inkooporder creëren ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Selecteer een leverancier uit de standaardleveranciers van de onderstaande items. Bij selectie wordt er alleen een inkooporder gemaakt voor artikelen van de geselecteerde leverancier., +Row #{}: You must select {} serial numbers for item {}.,Rij # {}: u moet {} serienummers voor artikel {} selecteren., diff --git a/erpnext/translations/no.csv b/erpnext/translations/no.csv index bb8dbf25f3..150e5ca4a2 100644 --- a/erpnext/translations/no.csv +++ b/erpnext/translations/no.csv @@ -4238,7 +4238,6 @@ Download as JSON,Last ned som JSON, End date can not be less than start date,Sluttdato kan ikke være mindre enn startdato, For Default Supplier (Optional),For standardleverandør (valgfritt), From date cannot be greater than To date,Fra dato ikke kan være større enn To Date, -Get items from,Få elementer fra, Group by,Grupper etter, In stock,På lager, Item name,Navn, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Innkjøpsordre Trender, Purchase Receipt Trends,Kvitteringen Trender, Purchase Register,Kjøp Register, Quotation Trends,Anførsels Trender, -Quoted Item Comparison,Sitert Element Sammenligning, Received Items To Be Billed,Mottatte elementer å bli fakturert, Qty to Order,Antall å bestille, Requested Items To Be Transferred,Etterspør elementene som skal overføres, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Varen det refereres til av Therapy Session overlaps with {0},Behandlingsøkten overlapper med {0}, Therapy Sessions Overlapping,Terapisessioner overlapper hverandre, Therapy Plans,Terapiplaner, +"Item Code, warehouse, quantity are required on row {0}","Varekode, lager, antall kreves på rad {0}", +Get Items from Material Requests against this Supplier,Få varer fra materialforespørsler mot denne leverandøren, +Enable European Access,Aktiver europeisk tilgang, +Creating Purchase Order ...,Opprette innkjøpsordre ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Velg en leverandør fra standardleverandørene av artiklene nedenfor. Ved valg vil det kun gjøres en innkjøpsordre mot varer som tilhører den valgte leverandøren., +Row #{}: You must select {} serial numbers for item {}.,Rad nr. {}: Du må velge {} serienumre for varen {}., diff --git a/erpnext/translations/pl.csv b/erpnext/translations/pl.csv index c30d055e02..8340b7272f 100644 --- a/erpnext/translations/pl.csv +++ b/erpnext/translations/pl.csv @@ -4238,7 +4238,6 @@ Download as JSON,Pobierz jako JSON, End date can not be less than start date,"Data zakończenia nie może być wcześniejsza, niż data rozpoczęcia", For Default Supplier (Optional),Dla dostawcy domyślnego (opcjonalnie), From date cannot be greater than To date,Data od - nie może być późniejsza niż Data do, -Get items from,Pobierz zawartość z, Group by,Grupuj według, In stock,W magazynie, Item name,Nazwa pozycji, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Trendy Zamówienia Kupna, Purchase Receipt Trends,Trendy Potwierdzenia Zakupu, Purchase Register,Rejestracja Zakupu, Quotation Trends,Trendy Wyceny, -Quoted Item Comparison,Porównanie cytowany Item, Received Items To Be Billed,Otrzymane przedmioty czekające na zaksięgowanie, Qty to Order,Ilość do zamówienia, Requested Items To Be Transferred,Proszę o Przetranferowanie Przedmiotów, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Pozycja, do której odwoł Therapy Session overlaps with {0},Sesja terapeutyczna pokrywa się z {0}, Therapy Sessions Overlapping,Nakładanie się sesji terapeutycznych, Therapy Plans,Plany terapii, +"Item Code, warehouse, quantity are required on row {0}","Kod pozycji, magazyn, ilość są wymagane w wierszu {0}", +Get Items from Material Requests against this Supplier,Pobierz pozycje z żądań materiałowych od tego dostawcy, +Enable European Access,Włącz dostęp w Europie, +Creating Purchase Order ...,Tworzenie zamówienia zakupu ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Wybierz dostawcę spośród domyślnych dostawców z poniższych pozycji. Po dokonaniu wyboru, Zamówienie zostanie złożone wyłącznie dla pozycji należących do wybranego Dostawcy.", +Row #{}: You must select {} serial numbers for item {}.,Wiersz nr {}: należy wybrać {} numery seryjne dla towaru {}., diff --git a/erpnext/translations/ps.csv b/erpnext/translations/ps.csv index d8a51aede8..1dcaf48d79 100644 --- a/erpnext/translations/ps.csv +++ b/erpnext/translations/ps.csv @@ -4238,7 +4238,6 @@ Download as JSON,د Json په څیر ډاونلوډ کړئ, End date can not be less than start date,د پای نیټه نه شي کولای په پرتله د پیل نیټه کمه وي, For Default Supplier (Optional),د اصلي عرضه کوونکي لپاره (اختیاري), From date cannot be greater than To date,د نیټې څخه د نیټې څخه تر ډیره لوی نه وي, -Get items from,له توکي ترلاسه کړئ, Group by,ډله په, In stock,په ګدام کښي, Item name,د قالب نوم, @@ -8549,7 +8548,6 @@ Purchase Order Trends,پیري نظم رجحانات, Purchase Receipt Trends,رانيول رسيد رجحانات, Purchase Register,رانيول د نوم ثبتول, Quotation Trends,د داوطلبۍ رجحانات, -Quoted Item Comparison,له خولې د قالب پرتله, Received Items To Be Billed,ترلاسه توکي چې د محاسبې ته شي, Qty to Order,Qty ته اخلي., Requested Items To Be Transferred,غوښتنه سامان ته انتقال شي, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,هغه توکی چې د {0} Therapy Session overlaps with {0},د درملنې ناسته د over 0 with سره پراخه کیږي, Therapy Sessions Overlapping,د تهيريپي ناستې پوړونه, Therapy Plans,د درملنې پلانونه, +"Item Code, warehouse, quantity are required on row {0}",د توکي کوډ ، ګودام ، مقدار په قطار کې اړین دي {0}, +Get Items from Material Requests against this Supplier,د دې عرضه کونکي په وړاندې د موادو غوښتنو څخه توکي ترلاسه کړئ, +Enable European Access,اروپایی لاسرسی وړ کړئ, +Creating Purchase Order ...,د پیرود امر رامینځته کول ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",د لاندې شیانو ډیفالټ عرضه کونکو څخه یو عرضه کونکی غوره کړئ. په انتخاب کولو کې ، د پیرود امر به یوازې هغه انتخاب شوي چمتو کونکي پورې اړه لرونکي توکو په مقابل کې وي., +Row #{}: You must select {} serial numbers for item {}.,قطار # {}: تاسو باید د توکي {{لپاره سریال نمبرونه وټاکئ., diff --git a/erpnext/translations/pt.csv b/erpnext/translations/pt.csv index 3289ef4f4c..3b8a0a0f43 100644 --- a/erpnext/translations/pt.csv +++ b/erpnext/translations/pt.csv @@ -4238,7 +4238,6 @@ Download as JSON,Baixe como Json, End date can not be less than start date,A Data de Término não pode ser mais recente que a Data de Início, For Default Supplier (Optional),Para fornecedor padrão (opcional), From date cannot be greater than To date,De data não pode ser maior que a data, -Get items from,Obter itens de, Group by,Agrupar Por, In stock,Em estoque, Item name,Nome do item, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Tendências de Ordens de Compra, Purchase Receipt Trends,Tendências de Recibo de Compra, Purchase Register,Registo de Compra, Quotation Trends,Tendências de Cotação, -Quoted Item Comparison,Comparação de Cotação de Item, Received Items To Be Billed,Itens Recebidos a Serem Faturados, Qty to Order,Qtd a Encomendar, Requested Items To Be Transferred,Itens Solicitados A Serem Transferidos, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,O item referenciado por {0} Therapy Session overlaps with {0},A sessão de terapia se sobrepõe a {0}, Therapy Sessions Overlapping,Sobreposição de sessões de terapia, Therapy Plans,Planos de Terapia, +"Item Code, warehouse, quantity are required on row {0}","Código do item, armazém, quantidade são necessários na linha {0}", +Get Items from Material Requests against this Supplier,Obtenha itens de solicitações de materiais contra este fornecedor, +Enable European Access,Habilitar acesso europeu, +Creating Purchase Order ...,Criando pedido de compra ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Selecione um fornecedor dos fornecedores padrão dos itens abaixo. Na seleção, um pedido de compra será feito apenas para itens pertencentes ao fornecedor selecionado.", +Row #{}: You must select {} serial numbers for item {}.,Nº da linha {}: você deve selecionar {} números de série para o item {}., diff --git a/erpnext/translations/pt_br.csv b/erpnext/translations/pt_br.csv index 620fe273c4..cda5ee85f9 100644 --- a/erpnext/translations/pt_br.csv +++ b/erpnext/translations/pt_br.csv @@ -985,6 +985,7 @@ Purpose must be one of {0},Objetivo deve ser um dos {0}, Qty,Qtde, Qty To Manufacture,Qtde para Fabricar, Qty for {0},Qtde para {0}, +Quality,QCคุณภาพ, Quality Goal.,Objetivos de Qualidade., Quality Inspection,Inspeção de Qualidade, Quality Inspection: {0} is not submitted for the item: {1} in row {2},Inspeção de Qualidade: {0} não foi submetida para o item: {1} na linha {2}, diff --git a/erpnext/translations/ro.csv b/erpnext/translations/ro.csv index 54211e2ee5..643b8c5c3e 100644 --- a/erpnext/translations/ro.csv +++ b/erpnext/translations/ro.csv @@ -4238,7 +4238,6 @@ Download as JSON,Descărcați ca JSON, End date can not be less than start date,Data de Incheiere nu poate fi anterioara Datei de Incepere, For Default Supplier (Optional),Pentru furnizor implicit (opțional), From date cannot be greater than To date,De la data nu poate fi mai mare decât la data, -Get items from,Obține elemente din, Group by,Grupul De, In stock,In stoc, Item name,Denumire Articol, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Comandă de aprovizionare Tendințe, Purchase Receipt Trends,Tendințe Primirea de cumpărare, Purchase Register,Cumpărare Inregistrare, Quotation Trends,Cotație Tendințe, -Quoted Item Comparison,Compararea Articol citat, Received Items To Be Billed,Articole primite Pentru a fi facturat, Qty to Order,Cantitate pentru comandă, Requested Items To Be Transferred,Articole solicitate de transferat, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Elementul la care face refe Therapy Session overlaps with {0},Sesiunea de terapie se suprapune cu {0}, Therapy Sessions Overlapping,Sesiunile de terapie se suprapun, Therapy Plans,Planuri de terapie, +"Item Code, warehouse, quantity are required on row {0}","Codul articolului, depozitul, cantitatea sunt necesare pe rândul {0}", +Get Items from Material Requests against this Supplier,Obțineți articole din solicitările materiale împotriva acestui furnizor, +Enable European Access,Activați accesul european, +Creating Purchase Order ...,Se creează comanda de cumpărare ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Selectați un furnizor din furnizorii prestabiliți ai articolelor de mai jos. La selectare, o comandă de achiziție va fi făcută numai pentru articolele aparținând furnizorului selectat.", +Row #{}: You must select {} serial numbers for item {}.,Rândul # {}: trebuie să selectați {} numere de serie pentru articol {}., diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 00a19b360c..7fcb7b08f7 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -4238,7 +4238,6 @@ Download as JSON,Скачать как JSON, End date can not be less than start date,"Дата окончания не может быть меньше, чем Дата начала", For Default Supplier (Optional),Поставщик по умолчанию (необязательно), From date cannot be greater than To date,"С даты не может быть больше, чем к дате", -Get items from,Получить продукты от, Group by,Group By, In stock,В наличии, Item name,Название продукта, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Заказ на покупку Тенденции, Purchase Receipt Trends,Динамика Получения Поставок, Purchase Register,Покупка Становиться на учет, Quotation Trends,Динамика предложений, -Quoted Item Comparison,Цитируется Сравнение товара, Received Items To Be Billed,"Полученные товары, на которые нужно выписать счет", Qty to Order,Кол-во в заказ, Requested Items To Be Transferred,Запрашиваемые продукты к доставке, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"На товар, на ко Therapy Session overlaps with {0},Сеанс терапии совпадает с {0}, Therapy Sessions Overlapping,Совмещение сеансов терапии, Therapy Plans,Планы терапии, +"Item Code, warehouse, quantity are required on row {0}","Код товара, склад, количество требуются в строке {0}", +Get Items from Material Requests against this Supplier,Получить товары из запросов материалов к этому поставщику, +Enable European Access,Включить европейский доступ, +Creating Purchase Order ...,Создание заказа на поставку ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Выберите поставщика из списка поставщиков по умолчанию для позиций ниже. При выборе Заказ на поставку будет сделан в отношении товаров, принадлежащих только выбранному Поставщику.", +Row #{}: You must select {} serial numbers for item {}.,Строка № {}: необходимо выбрать {} серийных номеров для позиции {}., diff --git a/erpnext/translations/rw.csv b/erpnext/translations/rw.csv index 5995db1719..64591399af 100644 --- a/erpnext/translations/rw.csv +++ b/erpnext/translations/rw.csv @@ -4238,7 +4238,6 @@ Download as JSON,Kuramo nka JSON, End date can not be less than start date,Itariki yo kurangiriraho ntishobora kuba munsi yitariki yo gutangiriraho, For Default Supplier (Optional),Kubisanzwe Bitanga (Bihitamo), From date cannot be greater than To date,Kuva ku italiki ntishobora kurenza Kumunsi, -Get items from,Shaka ibintu, Group by,Itsinda by, In stock,Mububiko, Item name,Izina ryikintu, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Kugura Ibiciro, Purchase Receipt Trends,Kugura inyemezabwishyu, Purchase Register,Kwiyandikisha, Quotation Trends,Imirongo, -Quoted Item Comparison,Kugereranya Ikintu Kugereranya, Received Items To Be Billed,Yakiriye Ibintu Byishyurwa, Qty to Order,Qty gutumiza, Requested Items To Be Transferred,Ibintu Byasabwe Kwimurwa, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Ikintu kivugwa na {0} - {1} Therapy Session overlaps with {0},Isomo ryo kuvura ryuzuzanya na {0}, Therapy Sessions Overlapping,Amasomo yo kuvura, Therapy Plans,Gahunda yo kuvura, +"Item Code, warehouse, quantity are required on row {0}","Kode yikintu, ububiko, ingano irakenewe kumurongo {0}", +Get Items from Material Requests against this Supplier,Shakisha Ibintu Mubisabwa Ibikoresho Kurwanya Utanga isoko, +Enable European Access,Gushoboza Uburayi, +Creating Purchase Order ...,Gushiraho gahunda yo kugura ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Hitamo Utanga isoko uhereye kubisanzwe bitanga ibintu hepfo. Muguhitamo, Iteka ryubuguzi rizakorwa kurwanya ibintu byatoranijwe gusa.", +Row #{}: You must select {} serial numbers for item {}.,Umurongo # {}: Ugomba guhitamo {} nimero yuruhererekane kubintu {}., diff --git a/erpnext/translations/si.csv b/erpnext/translations/si.csv index ba45b42006..690c47332d 100644 --- a/erpnext/translations/si.csv +++ b/erpnext/translations/si.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON ලෙස බාගන්න, End date can not be less than start date,අවසන් දිනය ඇරඹුම් දිනය ඊට වඩා අඩු විය නොහැක, For Default Supplier (Optional),Default සැපයුම්කරු සඳහා (විකල්ප), From date cannot be greater than To date,දිනය සිට දිනට වඩා වැඩි විය නොහැක, -Get items from,සිට භාණ්ඩ ලබා ගන්න, Group by,කණ්ඩායම විසින්, In stock,ගබඩාවේ ඇත, Item name,අයිතමය නම, @@ -8549,7 +8548,6 @@ Purchase Order Trends,මිලදී ගැනීමේ නියෝගයක Purchase Receipt Trends,මිලදී ගැනීම රිසිට්පත ප්රවණතා, Purchase Register,මිලදී රෙජිස්ටර්, Quotation Trends,උද්ධෘත ප්රවණතා, -Quoted Item Comparison,උපුටා අයිතමය සංසන්දනය, Received Items To Be Billed,ලැබී අයිතම බිල්පතක්, Qty to Order,ඇණවුම් යවන ලද, Requested Items To Be Transferred,ඉල්ලන අයිතම මාරු කර, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1 by විසින Therapy Session overlaps with {0},චිකිත්සක සැසිය {0 with සමඟ අතිච්ඡාදනය වේ, Therapy Sessions Overlapping,චිකිත්සක සැසි අතිච්ඡාදනය, Therapy Plans,චිකිත්සක සැලසුම්, +"Item Code, warehouse, quantity are required on row {0}","Code 0 row පේළියේ අයිතම කේතය, ගබඩාව, ප්‍රමාණය අවශ්‍ය වේ", +Get Items from Material Requests against this Supplier,මෙම සැපයුම්කරුට එරෙහිව ද්‍රව්‍යමය ඉල්ලීම් වලින් අයිතම ලබා ගන්න, +Enable European Access,යුරෝපීය ප්‍රවේශය සක්‍රීය කරන්න, +Creating Purchase Order ...,මිලදී ගැනීමේ ඇණවුමක් නිර්මාණය කිරීම ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","පහත අයිතමවල පෙරනිමි සැපයුම්කරුවන්ගෙන් සැපයුම්කරුවෙකු තෝරන්න. තෝරාගැනීමේදී, තෝරාගත් සැපයුම්කරුට පමණක් අයත් භාණ්ඩවලට එරෙහිව මිලදී ගැනීමේ නියෝගයක් කරනු ලැබේ.", +Row #{}: You must select {} serial numbers for item {}.,පේළිය # {}: ඔබ item item අයිතමය සඳහා {} අනුක්‍රමික අංක තෝරාගත යුතුය., diff --git a/erpnext/translations/sk.csv b/erpnext/translations/sk.csv index d31adbadc2..cb4a7fed4b 100644 --- a/erpnext/translations/sk.csv +++ b/erpnext/translations/sk.csv @@ -4238,7 +4238,6 @@ Download as JSON,Stiahnuť ako JSON, End date can not be less than start date,Dátum ukončenia nemôže byť menší ako dátum začiatku, For Default Supplier (Optional),Pre predvoleného dodávateľa (nepovinné), From date cannot be greater than To date,Dátum OD nemôže byť väčší ako dátum DO, -Get items from,Získať predmety z, Group by,Seskupit podle, In stock,Skladom, Item name,Názov položky, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Nákupní objednávka trendy, Purchase Receipt Trends,Doklad o koupi Trendy, Purchase Register,Nákup Register, Quotation Trends,Vývoje ponúk, -Quoted Item Comparison,Citoval Položka Porovnanie, Received Items To Be Billed,"Přijaté položek, které mají být účtovány", Qty to Order,Množství k objednávce, Requested Items To Be Transferred,Požadované položky mají být převedeny, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Položka, na ktorú odkazu Therapy Session overlaps with {0},Terapeutické sedenie sa prekrýva s {0}, Therapy Sessions Overlapping,Terapeutické sedenia sa prekrývajú, Therapy Plans,Terapeutické plány, +"Item Code, warehouse, quantity are required on row {0}","V riadku {0} sa vyžaduje kód položky, sklad, množstvo", +Get Items from Material Requests against this Supplier,Získajte položky z materiálových požiadaviek voči tomuto dodávateľovi, +Enable European Access,Umožniť európsky prístup, +Creating Purchase Order ...,Vytvára sa objednávka ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Z predvolených dodávateľov nižšie uvedených položiek vyberte dodávateľa. Pri výbere sa uskutoční objednávka iba na položky patriace vybranému dodávateľovi., +Row #{}: You must select {} serial numbers for item {}.,Riadok č. {}: Musíte zvoliť {} sériové čísla pre položku {}., diff --git a/erpnext/translations/sl.csv b/erpnext/translations/sl.csv index 15795dccf0..8beec6be16 100644 --- a/erpnext/translations/sl.csv +++ b/erpnext/translations/sl.csv @@ -4238,7 +4238,6 @@ Download as JSON,Prenesite kot JSON, End date can not be less than start date,Datum konca ne sme biti krajši od začetnega datuma, For Default Supplier (Optional),Za privzeto dobavitelja (neobvezno), From date cannot be greater than To date,Od datuma ne more biti večje od datuma, -Get items from,Pridobi artikle iz, Group by,Skupina avtorja, In stock,Na zalogi, Item name,Ime predmeta, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Naročilnica Trendi, Purchase Receipt Trends,Nakup Prejem Trendi, Purchase Register,Nakup Register, Quotation Trends,Trendi ponudb, -Quoted Item Comparison,Citirano Točka Primerjava, Received Items To Be Billed,Prejete Postavke placevali, Qty to Order,Količina naročiti, Requested Items To Be Transferred,Zahtevane blago prenaša, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Element, na katerega se sk Therapy Session overlaps with {0},Seja terapije se prekriva z {0}, Therapy Sessions Overlapping,Terapijske seje se prekrivajo, Therapy Plans,Načrti terapije, +"Item Code, warehouse, quantity are required on row {0}","Koda artikla, skladišče, količina so obvezni v vrstici {0}", +Get Items from Material Requests against this Supplier,Pridobite izdelke iz materialnih zahtevkov pri tem dobavitelju, +Enable European Access,Omogoči evropski dostop, +Creating Purchase Order ...,Ustvarjanje naročilnice ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Izberite dobavitelja med privzetimi dobavitelji spodnjih elementov. Po izbiri bo naročilnica narejena samo za izdelke, ki pripadajo izbranemu dobavitelju.", +Row #{}: You must select {} serial numbers for item {}.,Vrstica # {}: Izbrati morate {} serijske številke za izdelek {}., diff --git a/erpnext/translations/sq.csv b/erpnext/translations/sq.csv index 18d634e6c6..05aefa3602 100644 --- a/erpnext/translations/sq.csv +++ b/erpnext/translations/sq.csv @@ -4238,7 +4238,6 @@ Download as JSON,Shkarkoni si JSON, End date can not be less than start date,Data e mbarimit nuk mund të jetë më e shkurtër se data fillestare, For Default Supplier (Optional),Për Furnizuesin e Parazgjedhur (fakultativ), From date cannot be greater than To date,Nga Data nuk mund të jetë më i madh se deri më sot, -Get items from,Të marrë sendet nga, Group by,Grupi Nga, In stock,Në gjendje, Item name,Item Emri, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Rendit Blerje Trendet, Purchase Receipt Trends,Trendet Receipt Blerje, Purchase Register,Blerje Regjistrohu, Quotation Trends,Kuotimit Trendet, -Quoted Item Comparison,Cituar Item Krahasimi, Received Items To Be Billed,Items marra Për të faturohet, Qty to Order,Qty të Rendit, Requested Items To Be Transferred,Items kërkuar të transferohet, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Artikulli i referuar nga {0 Therapy Session overlaps with {0},Sesioni i terapisë mbivendoset me {0}, Therapy Sessions Overlapping,Seancat e Terapisë Mbivendosen, Therapy Plans,Planet e Terapisë, +"Item Code, warehouse, quantity are required on row {0}","Kodi i artikullit, depoja, sasia kërkohen në rreshtin {0}", +Get Items from Material Requests against this Supplier,Merrni Artikuj nga Kërkesat Materiale kundër këtij Furnizuesi, +Enable European Access,Aktivizo Aksesin Evropian, +Creating Purchase Order ...,Krijimi i urdhrit të blerjes ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Zgjidhni një furnitor nga furnitorët e parazgjedhur të artikujve më poshtë. Gjatë zgjedhjes, një Urdhër Blerje do të bëhet vetëm kundër artikujve që i përkasin vetëm Furnizuesit të zgjedhur.", +Row #{}: You must select {} serial numbers for item {}.,Rreshti # {}: Duhet të zgjidhni {} numrat rendorë për artikullin {}., diff --git a/erpnext/translations/sr.csv b/erpnext/translations/sr.csv index 44c49467b6..b507f74f09 100644 --- a/erpnext/translations/sr.csv +++ b/erpnext/translations/sr.csv @@ -4238,7 +4238,6 @@ Download as JSON,Преузми као ЈСОН, End date can not be less than start date,"Дата окончания не может быть меньше , чем Дата начала", For Default Supplier (Optional),За подразумевани добављач, From date cannot be greater than To date,Од датума не може бити већа него до сада, -Get items from,Гет ставке из, Group by,Група По, In stock,На лагеру, Item name,Назив, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Куповина Трендови Ордер, Purchase Receipt Trends,Куповина Трендови Пријем, Purchase Register,Куповина Регистрација, Quotation Trends,Котировочные тенденции, -Quoted Item Comparison,Цитирано артикла Поређење, Received Items To Be Billed,Примљени артикала буду наплаћени, Qty to Order,Количина по поруџбини, Requested Items To Be Transferred,Тражени Артикли ће се пренети, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Ставка на коју Therapy Session overlaps with {0},Сесија терапије се преклапа са {0}, Therapy Sessions Overlapping,Преклапање сесија терапије, Therapy Plans,Планови терапије, +"Item Code, warehouse, quantity are required on row {0}","Шифра артикла, складиште, количина су обавезни у реду {0}", +Get Items from Material Requests against this Supplier,Узмите предмете од материјалних захтева против овог добављача, +Enable European Access,Омогућити европски приступ, +Creating Purchase Order ...,Креирање налога за куповину ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Изаберите добављача од задатих добављача доле наведених ставки. Након одабира, наруџбеница ће се извршити само за производе који припадају изабраном добављачу.", +Row #{}: You must select {} serial numbers for item {}.,Ред # {}: Морате одабрати {} серијске бројеве за ставку {}., diff --git a/erpnext/translations/sr_sp.csv b/erpnext/translations/sr_sp.csv index 3304dfcf26..5e7ae79781 100644 --- a/erpnext/translations/sr_sp.csv +++ b/erpnext/translations/sr_sp.csv @@ -649,7 +649,6 @@ No,Ne, Yes,Da, Chart of Accounts,Kontni plan, Customer database.,Korisnička baza podataka, -Get items from,Dodaj stavke iz, Item name,Naziv artikla, No employee found,Zaposleni nije pronađen, Open Projects ,Otvoreni projekti, diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv index 608bb46577..57e02792f4 100644 --- a/erpnext/translations/sv.csv +++ b/erpnext/translations/sv.csv @@ -4238,7 +4238,6 @@ Download as JSON,Ladda ner som JSON, End date can not be less than start date,Slutdatum kan inte vara mindre än startdatum, For Default Supplier (Optional),För standardleverantör (tillval), From date cannot be greater than To date,Från datum kan inte vara större än till datum, -Get items from,Få objekt från, Group by,Gruppera efter, In stock,I lager, Item name,Produktnamn, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Inköpsorder Trender, Purchase Receipt Trends,Kvitto Trender, Purchase Register,Inköpsregistret, Quotation Trends,Offert Trender, -Quoted Item Comparison,Citerade föremål Jämförelse, Received Items To Be Billed,Mottagna objekt som ska faktureras, Qty to Order,Antal till Ordern, Requested Items To Be Transferred,Efterfrågade artiklar som ska överföras, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Varan som {0} - {1} referer Therapy Session overlaps with {0},Terapisessionen överlappar med {0}, Therapy Sessions Overlapping,Terapisessioner överlappar varandra, Therapy Plans,Terapiplaner, +"Item Code, warehouse, quantity are required on row {0}","Artikelkod, lager, kvantitet krävs på rad {0}", +Get Items from Material Requests against this Supplier,Få objekt från materialförfrågningar mot denna leverantör, +Enable European Access,Aktivera europeisk åtkomst, +Creating Purchase Order ...,Skapar inköpsorder ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",Välj en leverantör bland standardleverantörerna av artiklarna nedan. Vid val kommer en inköpsorder endast göras mot föremål som tillhör vald leverantör., +Row #{}: You must select {} serial numbers for item {}.,Rad nr {}: Du måste välja {} serienummer för artikeln {}., diff --git a/erpnext/translations/sw.csv b/erpnext/translations/sw.csv index bffc1a8d47..3595727666 100644 --- a/erpnext/translations/sw.csv +++ b/erpnext/translations/sw.csv @@ -4238,7 +4238,6 @@ Download as JSON,Pakua kama JSON, End date can not be less than start date,Tarehe ya Mwisho haiwezi kuwa chini ya Tarehe ya Mwanzo, For Default Supplier (Optional),Kwa Default Supplier (hiari), From date cannot be greater than To date,Kutoka Tarehe haiwezi kuwa kubwa kuliko Tarehe, -Get items from,Pata vitu kutoka, Group by,Kikundi Kwa, In stock,Katika hisa, Item name,Jina la Kipengee, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Mwelekeo wa Utaratibu wa Ununuzi, Purchase Receipt Trends,Ununuzi Mwelekeo wa Receipt, Purchase Register,Daftari ya Ununuzi, Quotation Trends,Mwelekeo wa Nukuu, -Quoted Item Comparison,Ilipendekeza Kulinganishwa kwa Bidhaa, Received Items To Be Billed,Vipokee Vipokee vya Kulipwa, Qty to Order,Uchina kwa Amri, Requested Items To Be Transferred,Vitu Vilivyoombwa Ili Kuhamishwa, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Bidhaa iliyorejelewa na {0} Therapy Session overlaps with {0},Kipindi cha Tiba hupishana na {0}, Therapy Sessions Overlapping,Vikao vya Tiba vinaingiliana, Therapy Plans,Mipango ya Tiba, +"Item Code, warehouse, quantity are required on row {0}","Nambari ya Bidhaa, ghala, idadi inahitajika kwenye safu mlalo {0}", +Get Items from Material Requests against this Supplier,Pata Vitu kutoka kwa Maombi ya Nyenzo dhidi ya Muuzaji huyu, +Enable European Access,Washa Ufikiaji wa Uropa, +Creating Purchase Order ...,Inaunda Agizo la Ununuzi ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Chagua Muuzaji kutoka kwa Wasambazaji Default wa vitu hapa chini. Wakati wa kuchagua, Agizo la Ununuzi litafanywa dhidi ya vitu vya Muuzaji aliyechaguliwa tu.", +Row #{}: You must select {} serial numbers for item {}.,Mstari # {}: Lazima uchague {} nambari za serial za kipengee {}., diff --git a/erpnext/translations/ta.csv b/erpnext/translations/ta.csv index b57668d8cf..100f0e9300 100644 --- a/erpnext/translations/ta.csv +++ b/erpnext/translations/ta.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON ஆக பதிவிறக்கவும், End date can not be less than start date,முடிவு தேதி தொடங்கும் நாள் விட குறைவாக இருக்க முடியாது, For Default Supplier (Optional),இயல்புநிலை வழங்குநருக்கு (விரும்பினால்), From date cannot be greater than To date,தேதி முதல் இன்று வரை விட முடியாது, -Get items from,இருந்து பொருட்களை பெற, Group by,குழு மூலம், In stock,கையிருப்பில், Item name,பொருள் பெயர், @@ -8549,7 +8548,6 @@ Purchase Order Trends,ஆர்டர் போக்குகள் வா Purchase Receipt Trends,ரிசிப்ட் போக்குகள் வாங்குவதற்கு, Purchase Register,பதிவு வாங்குவதற்கு, Quotation Trends,மேற்கோள் போக்குகள், -Quoted Item Comparison,மேற்கோள் காட்டப்பட்டது பொருள் ஒப்பீட்டு, Received Items To Be Billed,கட்டணம் பெறப்படும் பொருட்கள், Qty to Order,அளவு ஒழுங்கிற்கு, Requested Items To Be Transferred,மாற்றப்படுவதற்கு கோரப்பட்ட விடயங்கள், @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1 by ஆல் க Therapy Session overlaps with {0},சிகிச்சை அமர்வு {0 with உடன் ஒன்றுடன் ஒன்று, Therapy Sessions Overlapping,சிகிச்சை அமர்வுகள் ஒன்றுடன் ஒன்று, Therapy Plans,சிகிச்சை திட்டங்கள், +"Item Code, warehouse, quantity are required on row {0}","Code 0 row வரிசையில் உருப்படி குறியீடு, கிடங்கு, அளவு தேவை", +Get Items from Material Requests against this Supplier,இந்த சப்ளையருக்கு எதிரான பொருள் கோரிக்கைகளிலிருந்து பொருட்களைப் பெறுங்கள், +Enable European Access,ஐரோப்பிய அணுகலை இயக்கு, +Creating Purchase Order ...,கொள்முதல் ஆணையை உருவாக்குதல் ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","கீழே உள்ள பொருட்களின் இயல்புநிலை சப்ளையர்களிடமிருந்து ஒரு சப்ளையரைத் தேர்ந்தெடுக்கவும். தேர்ந்தெடுக்கும் போது, தேர்ந்தெடுக்கப்பட்ட சப்ளையருக்கு மட்டுமே சொந்தமான பொருட்களுக்கு எதிராக கொள்முதல் ஆணை செய்யப்படும்.", +Row #{}: You must select {} serial numbers for item {}.,வரிசை # {}: நீங்கள் item item உருப்படிக்கு {} வரிசை எண்களைத் தேர்ந்தெடுக்க வேண்டும்., diff --git a/erpnext/translations/te.csv b/erpnext/translations/te.csv index e5ba32c2f5..047d07731e 100644 --- a/erpnext/translations/te.csv +++ b/erpnext/translations/te.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON గా డౌన్‌లోడ్ చేయండి, End date can not be less than start date,ముగింపు తేదీ ప్రారంభ తేదీ కంటే తక్కువ ఉండకూడదు, For Default Supplier (Optional),డిఫాల్ట్ సరఫరాదారు (ఐచ్ఛిక) కోసం, From date cannot be greater than To date,తేదీ నుండి తేదీ వరకు ఎక్కువ ఉండకూడదు, -Get items from,నుండి అంశాలను పొందండి, Group by,గ్రూప్ ద్వారా, In stock,అందుబాటులో ఉంది, Item name,అంశం పేరు, @@ -8549,7 +8548,6 @@ Purchase Order Trends,ఆర్డర్ ట్రెండ్లులో క Purchase Receipt Trends,కొనుగోలు రసీదులు ట్రెండ్లులో, Purchase Register,కొనుగోలు నమోదు, Quotation Trends,కొటేషన్ ట్రెండ్లులో, -Quoted Item Comparison,ఉల్లేఖించిన అంశం పోలిక, Received Items To Be Billed,స్వీకరించిన అంశాలు బిల్ టు, Qty to Order,ఆర్డర్ చేయటం అంశాల, Requested Items To Be Transferred,అభ్యర్థించిన అంశాలు బదిలీ, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1 by చే సూ Therapy Session overlaps with {0},థెరపీ సెషన్ {0 with తో అతివ్యాప్తి చెందుతుంది, Therapy Sessions Overlapping,థెరపీ సెషన్స్ అతివ్యాప్తి, Therapy Plans,చికిత్స ప్రణాళికలు, +"Item Code, warehouse, quantity are required on row {0}","Code 0 row వరుసలో అంశం కోడ్, గిడ్డంగి, పరిమాణం అవసరం", +Get Items from Material Requests against this Supplier,ఈ సరఫరాదారుకు వ్యతిరేకంగా మెటీరియల్ అభ్యర్థనల నుండి అంశాలను పొందండి, +Enable European Access,యూరోపియన్ ప్రాప్యతను ప్రారంభించండి, +Creating Purchase Order ...,కొనుగోలు క్రమాన్ని సృష్టిస్తోంది ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","దిగువ వస్తువుల డిఫాల్ట్ సరఫరాదారుల నుండి సరఫరాదారుని ఎంచుకోండి. ఎంపికపై, ఎంచుకున్న సరఫరాదారుకు చెందిన వస్తువులపై మాత్రమే కొనుగోలు ఆర్డర్ చేయబడుతుంది.", +Row #{}: You must select {} serial numbers for item {}.,అడ్డు వరుస # {}: మీరు తప్పక item} అంశం కోసం {} క్రమ సంఖ్యలను ఎంచుకోవాలి., diff --git a/erpnext/translations/th.csv b/erpnext/translations/th.csv index 90c3a9a6ad..71233ec8fa 100644 --- a/erpnext/translations/th.csv +++ b/erpnext/translations/th.csv @@ -4238,7 +4238,6 @@ Download as JSON,ดาวน์โหลดเป็น JSON, End date can not be less than start date,วันที่สิ้นสุด ไม่สามารถ จะน้อยกว่า วันเริ่มต้น, For Default Supplier (Optional),สำหรับผู้จัดจำหน่ายเริ่มต้น (ตัวเลือก), From date cannot be greater than To date,จากวันที่ไม่สามารถมากกว่าวันที่, -Get items from,รับรายการจาก, Group by,กลุ่มตาม, In stock,มีสินค้าในสต๊อก, Item name,ชื่อรายการ, @@ -8549,7 +8548,6 @@ Purchase Order Trends,แนวโน้มการสั่งซื้อ, Purchase Receipt Trends,ซื้อแนวโน้มใบเสร็จรับเงิน, Purchase Register,สั่งซื้อสมัครสมาชิก, Quotation Trends,ใบเสนอราคา แนวโน้ม, -Quoted Item Comparison,เปรียบเทียบรายการที่ยกมา, Received Items To Be Billed,รายการที่ได้รับจะถูกเรียกเก็บเงิน, Qty to Order,จำนวนการสั่งซื้อสินค้า, Requested Items To Be Transferred,รายการที่ได้รับการร้องขอจะถูกถ่ายโอน, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,รายการที่ Therapy Session overlaps with {0},เซสชันการบำบัดทับซ้อนกับ {0}, Therapy Sessions Overlapping,การบำบัดที่ทับซ้อนกัน, Therapy Plans,แผนการบำบัด, +"Item Code, warehouse, quantity are required on row {0}",ต้องระบุรหัสสินค้าคลังสินค้าปริมาณในแถว {0}, +Get Items from Material Requests against this Supplier,รับรายการจากคำขอวัสดุกับซัพพลายเออร์นี้, +Enable European Access,เปิดใช้งาน European Access, +Creating Purchase Order ...,กำลังสร้างใบสั่งซื้อ ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",เลือกซัพพลายเออร์จากซัพพลายเออร์เริ่มต้นของรายการด้านล่าง ในการเลือกใบสั่งซื้อจะทำกับสินค้าที่เป็นของซัพพลายเออร์ที่เลือกเท่านั้น, +Row #{}: You must select {} serial numbers for item {}.,แถว # {}: คุณต้องเลือก {} หมายเลขซีเรียลสำหรับรายการ {}, diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv index ebb754a217..9e7ba4d142 100644 --- a/erpnext/translations/tr.csv +++ b/erpnext/translations/tr.csv @@ -4238,7 +4238,6 @@ Download as JSON,JSON olarak indir, End date can not be less than start date,"Bitiş Tarihi, Başlangıç Tarihinden daha az olamaz", For Default Supplier (Optional),Varsayılan Tedarikçi için (İsteğe bağlı), From date cannot be greater than To date,Tarihten itibaren tarihe kadardan ileride olamaz, -Get items from,Öğeleri alın, Group by,Grup tarafından, In stock,Stokta var, Item name,Ürün Adı, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Satın Alma Sipariş Analizi, Purchase Receipt Trends,Satın Alma Teslim Alma Analizi, Purchase Register,Satın alma kaydı, Quotation Trends,Teklif Trendleri, -Quoted Item Comparison,Kote Ürün Karşılaştırma, Received Items To Be Billed,Faturalanacak Alınan Malzemeler, Qty to Order,Sipariş Miktarı, Requested Items To Be Transferred,Transfer edilmesi istenen Ürünler, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} tarafından refer Therapy Session overlaps with {0},"Terapi Oturumu, {0} ile çakışıyor", Therapy Sessions Overlapping,Çakışan Terapi Seansları, Therapy Plans,Tedavi Planları, +"Item Code, warehouse, quantity are required on row {0}","{0}. Satırda Öğe Kodu, depo, miktar gerekli", +Get Items from Material Requests against this Supplier,Bu Tedarikçiye Karşı Malzeme Taleplerinden Ürün Alın, +Enable European Access,Avrupa Erişimini Etkinleştir, +Creating Purchase Order ...,Satın Alma Siparişi Oluşturuluyor ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Aşağıdaki öğelerin Varsayılan Tedarikçilerinden bir Tedarikçi seçin. Seçim üzerine, yalnızca seçilen Tedarikçiye ait ürünler için bir Satın Alma Siparişi verilecektir.", +Row #{}: You must select {} serial numbers for item {}.,Satır # {}: {} öğesi için {} seri numaralarını seçmelisiniz., diff --git a/erpnext/translations/uk.csv b/erpnext/translations/uk.csv index b43346c414..53e2df518c 100644 --- a/erpnext/translations/uk.csv +++ b/erpnext/translations/uk.csv @@ -4238,7 +4238,6 @@ Download as JSON,Завантажити як JSON, End date can not be less than start date,"Дата завершення не може бути меншою, ніж Дата початку", For Default Supplier (Optional),Для постачальника за замовчуванням (необов'язково), From date cannot be greater than To date,"Від дати не може бути більше, ніж Дата", -Get items from,Отримати елементи з, Group by,Групувати за, In stock,В наявності, Item name,Назва виробу, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Динаміка Замовлень на придбанн Purchase Receipt Trends,Тренд прихідних накладних, Purchase Register,Реєстр закупівель, Quotation Trends,Тренд пропозицій, -Quoted Item Comparison,Цитується Порівняння товару, Received Items To Be Billed,"Отримані позиції, на які не виставлені рахунки", Qty to Order,К-сть для замовлення, Requested Items To Be Transferred,"Номенклатура, що ми замовили але не отримали", @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,"Позиція, на як Therapy Session overlaps with {0},Сеанс терапії накладається на {0}, Therapy Sessions Overlapping,"Сеанси терапії, що перекриваються", Therapy Plans,Плани терапії, +"Item Code, warehouse, quantity are required on row {0}","Код товару, склад, кількість вказуються в рядку {0}", +Get Items from Material Requests against this Supplier,Отримуйте предмети від матеріальних запитів у цього постачальника, +Enable European Access,Увімкнути європейський доступ, +Creating Purchase Order ...,Створення замовлення на придбання ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Виберіть постачальника з постачальників за замовчуванням із наведених нижче пунктів. При виборі замовлення на замовлення буде зроблено лише щодо товарів, що належать обраному Постачальнику.", +Row #{}: You must select {} serial numbers for item {}.,Рядок № {}: Ви повинні вибрати {} серійні номери для товару {}., diff --git a/erpnext/translations/ur.csv b/erpnext/translations/ur.csv index 7d9cacd272..aaaef5895f 100644 --- a/erpnext/translations/ur.csv +++ b/erpnext/translations/ur.csv @@ -4238,7 +4238,6 @@ Download as JSON,Json کی طرح ڈاؤن لوڈ کریں۔, End date can not be less than start date,ختم ہونے کی تاریخ شروع کرنے کی تاریخ کے مقابلے میں کم نہیں ہو سکتا, For Default Supplier (Optional),ڈیفالٹ سپلائر کے لئے (اختیاری), From date cannot be greater than To date,تاریخ سے تاریخ سے زیادہ نہیں ہوسکتی ہے, -Get items from,سے اشیاء حاصل, Group by,گروپ سے, In stock,اسٹاک میں, Item name,نام شے, @@ -8549,7 +8548,6 @@ Purchase Order Trends,آرڈر رجحانات خریدیں, Purchase Receipt Trends,خریداری کی رسید رجحانات, Purchase Register,خریداری رجسٹر, Quotation Trends,کوٹیشن رجحانات, -Quoted Item Comparison,نقل آئٹم موازنہ, Received Items To Be Billed,موصول ہونے والی اشیاء بل بھیجا جائے کرنے کے لئے, Qty to Order,آرڈر کی مقدار, Requested Items To Be Transferred,درخواست کی اشیاء منتقل کیا جائے, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,جس شے کا حوالہ { Therapy Session overlaps with {0},تھراپی سیشن {0 with کے ساتھ اوورلیپ ہوتا ہے, Therapy Sessions Overlapping,تھراپی سیشن اوور لیپنگ, Therapy Plans,تھراپی کے منصوبے, +"Item Code, warehouse, quantity are required on row {0}",آئٹم کوڈ ، گودام ، مقدار قطار میں ضروری ہے {0}, +Get Items from Material Requests against this Supplier,اس سپلائر کے خلاف مادی درخواستوں سے اشیا حاصل کریں, +Enable European Access,یورپی رسائی کو فعال کریں, +Creating Purchase Order ...,خریداری کا آرڈر تشکیل دے رہا ہے…, +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",ذیل میں آئٹمز کے ڈیفالٹ سپلائرز میں سے ایک سپلائر منتخب کریں۔ انتخاب پر ، خریداری کا آرڈر صرف منتخب کردہ سپلائر سے متعلقہ اشیاء کے خلاف کیا جائے گا۔, +Row #{}: You must select {} serial numbers for item {}.,قطار # {}: آپ کو آئٹم for for کے لئے}} سیریل نمبرز منتخب کرنا ہوں گے۔, diff --git a/erpnext/translations/uz.csv b/erpnext/translations/uz.csv index f05b582624..c983797aae 100644 --- a/erpnext/translations/uz.csv +++ b/erpnext/translations/uz.csv @@ -4238,7 +4238,6 @@ Download as JSON,Json sifatida yuklab oling, End date can not be less than start date,Tugash sanasi Boshlanish sanasidan past bo'lishi mumkin emas, For Default Supplier (Optional),Standart yetkazib beruvchi (ixtiyoriy), From date cannot be greater than To date,Sana Sana Sana uchun katta bo'lishi mumkin emas, -Get items from,Elementlarni oling, Group by,Guruh tomonidan, In stock,Omborda mavjud; sotuvda mavjud, Item name,Mavzu nomi, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Buyurtma tendentsiyalarini sotib olish, Purchase Receipt Trends,Qabul rejasi xaridlari, Purchase Register,Xarid qilish Register, Quotation Trends,Iqtiboslar tendentsiyalari, -Quoted Item Comparison,Qisqartirilgan ob'ektni solishtirish, Received Items To Be Billed,Qabul qilinadigan buyumlar, Qty to Order,Buyurtma miqdori, Requested Items To Be Transferred,Talab qilingan narsalarni yuborish, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0} - {1} tomonidan havola Therapy Session overlaps with {0},Terapiya mashg'uloti {0} bilan takrorlanadi, Therapy Sessions Overlapping,Terapiya mashg'ulotlari bir-birini qoplaydi, Therapy Plans,Terapiya rejalari, +"Item Code, warehouse, quantity are required on row {0}","Mahsulot kodi, ombor, miqdori {0} qatorida talab qilinadi", +Get Items from Material Requests against this Supplier,Ushbu etkazib beruvchiga qarshi material talablaridan narsalarni oling, +Enable European Access,Evropaga kirishni yoqish, +Creating Purchase Order ...,Xarid buyurtmasi yaratilmoqda ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Quyidagi mahsulotlarning standart etkazib beruvchilardan etkazib beruvchini tanlang. Tanlovda, faqat tanlangan etkazib beruvchiga tegishli buyumlarga qarshi Sotib olish to'g'risida buyurtma beriladi.", +Row #{}: You must select {} serial numbers for item {}.,Qator # {}: Siz {} element uchun seriya raqamlarini {} tanlashingiz kerak., diff --git a/erpnext/translations/vi.csv b/erpnext/translations/vi.csv index a76c9fdec6..03ff2ccc38 100644 --- a/erpnext/translations/vi.csv +++ b/erpnext/translations/vi.csv @@ -4238,7 +4238,6 @@ Download as JSON,Tải xuống dưới dạng JSON, End date can not be less than start date,Ngày kết thúc không thể nhỏ hơn Bắt đầu ngày, For Default Supplier (Optional),Đối với nhà cung cấp mặc định (Tùy chọn), From date cannot be greater than To date,"""Từ ngày"" không có thể lớn hơn ""Đến ngày""", -Get items from,Lấy dữ liệu từ, Group by,Nhóm theo, In stock,Trong kho, Item name,Tên hàng, @@ -8549,7 +8548,6 @@ Purchase Order Trends,Xu hướng mua hàng, Purchase Receipt Trends,Xu hướng của biên lai nhận hàng, Purchase Register,Đăng ký mua, Quotation Trends,Các Xu hướng dự kê giá, -Quoted Item Comparison,So sánh mẫu hàng đã được báo giá, Received Items To Be Billed,Những mẫu hàng nhận được để lập hóa đơn, Qty to Order,Số lượng đặt hàng, Requested Items To Be Transferred,Mục yêu cầu được chuyển giao, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,Mặt hàng được tham c Therapy Session overlaps with {0},Phiên trị liệu trùng lặp với {0}, Therapy Sessions Overlapping,Các phiên trị liệu chồng chéo, Therapy Plans,Kế hoạch trị liệu, +"Item Code, warehouse, quantity are required on row {0}","Mã hàng, kho, số lượng là bắt buộc trên hàng {0}", +Get Items from Material Requests against this Supplier,Nhận các mặt hàng từ các Yêu cầu Vật liệu đối với Nhà cung cấp này, +Enable European Access,Bật quyền truy cập Châu Âu, +Creating Purchase Order ...,Tạo Đơn đặt hàng ..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Chọn Nhà cung cấp từ các Nhà cung cấp mặc định của các mục dưới đây. Khi lựa chọn, Đơn đặt hàng sẽ được thực hiện đối với các mặt hàng chỉ thuộc về Nhà cung cấp đã chọn.", +Row #{}: You must select {} serial numbers for item {}.,Hàng # {}: Bạn phải chọn {} số sê-ri cho mặt hàng {}., diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv index 97c69fecb9..716f1f2f31 100644 --- a/erpnext/translations/zh.csv +++ b/erpnext/translations/zh.csv @@ -4238,7 +4238,6 @@ Download as JSON,下载为JSON, End date can not be less than start date,结束日期不能小于开始日期, For Default Supplier (Optional),对于默认供应商(可选), From date cannot be greater than To date,从日期不能大于到日期, -Get items from,从...获取物料, Group by,分组基于, In stock,有现货, Item name,物料名称, @@ -8549,7 +8548,6 @@ Purchase Order Trends,采购订单趋势, Purchase Receipt Trends,采购收货趋势, Purchase Register,采购台帐, Quotation Trends,报价趋势, -Quoted Item Comparison,项目报价比较, Received Items To Be Billed,待开费用清单已收货物料, Qty to Order,待下单数量, Requested Items To Be Transferred,已申请待移转物料, @@ -9834,3 +9832,9 @@ The item referenced by {0} - {1} is already invoiced,{0}-{1}引用的商品已 Therapy Session overlaps with {0},治疗会话与{0}重叠, Therapy Sessions Overlapping,治疗会议重叠, Therapy Plans,治疗计划, +"Item Code, warehouse, quantity are required on row {0}",在第{0}行中需要提供物料代码,仓库,数量, +Get Items from Material Requests against this Supplier,从针对此供应商的物料请求中获取物料, +Enable European Access,启用欧洲访问, +Creating Purchase Order ...,创建采购订单..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",从以下各项的默认供应商中选择供应商。选择后,将针对仅属于所选供应商的项目下达采购订单。, +Row #{}: You must select {} serial numbers for item {}.,行号{}:您必须为项目{}选择{}序列号。, diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv index 2de0df0d55..1cc7d8773e 100644 --- a/erpnext/translations/zh_tw.csv +++ b/erpnext/translations/zh_tw.csv @@ -3967,7 +3967,6 @@ Download as JSON,下載為JSON, End date can not be less than start date,結束日期不能小於開始日期, For Default Supplier (Optional),對於默認供應商(可選), From date cannot be greater than To date,起始日期不能大於結束日期, -Get items from,取得項目來源, Group by,集團通過, In stock,有現貨, Item name,項目名稱, @@ -7967,7 +7966,6 @@ Purchase Order Trends,採購訂單趨勢, Purchase Receipt Trends,採購入庫趨勢, Purchase Register,購買註冊, Quotation Trends,報價趨勢, -Quoted Item Comparison,項目報價比較, Received Items To Be Billed,待付款的收受品項, Qty to Order,訂購數量, Requested Items To Be Transferred,將要轉倉的需求項目, @@ -9136,3 +9134,9 @@ The item referenced by {0} - {1} is already invoiced,{0}-{1}引用的商品已 Therapy Session overlaps with {0},治療會話與{0}重疊, Therapy Sessions Overlapping,治療會議重疊, Therapy Plans,治療計劃, +"Item Code, warehouse, quantity are required on row {0}",在第{0}行中需要提供物料代碼,倉庫,數量, +Get Items from Material Requests against this Supplier,從針對此供應商的物料請求中獲取物料, +Enable European Access,啟用歐洲訪問, +Creating Purchase Order ...,創建採購訂單..., +"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.",從以下各項的默認供應商中選擇供應商。選擇後,將針對僅屬於所選供應商的項目下達採購訂單。, +Row #{}: You must select {} serial numbers for item {}.,行號{}:您必須為項目{}選擇{}序列號。, From 30d58cc3d741f11c7c5a6e57c0464941289943ce Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 9 Nov 2020 18:41:03 +0530 Subject: [PATCH 129/229] feat: Consider Holiday List in Student Leave Application and Attendance (#23388) * feat: Consider holiday list in Student Attendance and Leave Application * feat: Show Holidays as 'H' in Student Monthly Attendance Sheet * fix: check if date is a holiday in attendance reports * test: skip attendance record creation for holidays * fix: holiday list validation * fix: clean up after test * fix: codacy * fix: show date in user format * fix: remove ununsed imports * fix: sider * fix: test Co-authored-by: Nabin Hait --- .../student_attendance/student_attendance.py | 20 +++++- .../student_attendance_tool.py | 8 +-- .../student_leave_application.json | 9 ++- .../student_leave_application.py | 30 +++++++- .../test_student_leave_application.py | 71 +++++++++++++++---- .../absent_student_report.py | 9 ++- .../student_batch_wise_attendance.py | 8 ++- .../student_monthly_attendance_sheet.py | 26 ++++++- 8 files changed, 158 insertions(+), 23 deletions(-) diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py index 72a8f55c66..2e9e6cf8d6 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.py +++ b/erpnext/education/doctype/student_attendance/student_attendance.py @@ -6,8 +6,10 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import get_link_to_form, getdate +from frappe.utils import get_link_to_form, getdate, formatdate +from erpnext import get_default_company from erpnext.education.api import get_student_group_students +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday class StudentAttendance(Document): def validate(self): @@ -17,6 +19,7 @@ class StudentAttendance(Document): self.set_student_group() self.validate_student() self.validate_duplication() + self.validate_is_holiday() def set_date(self): if self.course_schedule: @@ -78,3 +81,18 @@ class StudentAttendance(Document): record = get_link_to_form('Student Attendance', attendance_record) frappe.throw(_('Student Attendance record {0} already exists against the Student {1}') .format(record, frappe.bold(self.student)), title=_('Duplicate Entry')) + + def validate_is_holiday(self): + holiday_list = get_holiday_list() + if is_holiday(holiday_list, self.date): + frappe.throw(_('Attendance cannot be marked for {0} as it is a holiday.').format( + frappe.bold(formatdate(self.date)))) + +def get_holiday_list(company=None): + if not company: + company = get_default_company() or frappe.get_all('Company')[0].name + + holiday_list = frappe.get_cached_value('Company', company, 'default_holiday_list') + if not holiday_list: + frappe.throw(_('Please set a default Holiday List for Company {0}').format(frappe.bold(get_default_company()))) + return holiday_list diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py index be2644077a..028db91881 100644 --- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py +++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py @@ -20,10 +20,10 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \ filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") - if not student_list: - student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , + if not student_list: + student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") - + if course_schedule: student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \ course_schedule= %s''', (course_schedule), as_dict=1) @@ -32,7 +32,7 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour student_group= %s and date= %s and \ (course_schedule is Null or course_schedule='')''', (student_group, date), as_dict=1) - + for attendance in student_attendance_list: for student in student_list: if student.student == attendance.student: diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application.json b/erpnext/education/doctype/student_leave_application/student_leave_application.json index ad5397629b..31b3da2fbd 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application.json +++ b/erpnext/education/doctype/student_leave_application/student_leave_application.json @@ -11,6 +11,7 @@ "column_break_3", "from_date", "to_date", + "total_leave_days", "section_break_5", "attendance_based_on", "student_group", @@ -110,11 +111,17 @@ { "fieldname": "column_break_11", "fieldtype": "Column Break" + }, + { + "fieldname": "total_leave_days", + "fieldtype": "Float", + "label": "Total Leave Days", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-07-08 13:22:38.329002", + "modified": "2020-09-21 18:10:24.440669", "modified_by": "Administrator", "module": "Education", "name": "Student Leave Application", diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application.py b/erpnext/education/doctype/student_leave_application/student_leave_application.py index c8841c999a..ef670124c3 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/student_leave_application.py @@ -6,11 +6,14 @@ from __future__ import unicode_literals import frappe from frappe import _ from datetime import timedelta -from frappe.utils import get_link_to_form, getdate +from frappe.utils import get_link_to_form, getdate, date_diff, flt +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday +from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list from frappe.model.document import Document class StudentLeaveApplication(Document): def validate(self): + self.validate_holiday_list() self.validate_duplicate() self.validate_from_to_dates('from_date', 'to_date') @@ -39,10 +42,19 @@ class StudentLeaveApplication(Document): frappe.throw(_('Leave application {0} already exists against the student {1}') .format(link, frappe.bold(self.student)), title=_('Duplicate Entry')) + def validate_holiday_list(self): + holiday_list = get_holiday_list() + self.total_leave_days = get_number_of_leave_days(self.from_date, self.to_date, holiday_list) + def update_attendance(self): + holiday_list = get_holiday_list() + for dt in daterange(getdate(self.from_date), getdate(self.to_date)): date = dt.strftime('%Y-%m-%d') + if is_holiday(holiday_list, date): + continue + attendance = frappe.db.exists('Student Attendance', { 'student': self.student, 'date': date, @@ -89,3 +101,19 @@ class StudentLeaveApplication(Document): def daterange(start_date, end_date): for n in range(int ((end_date - start_date).days)+1): yield start_date + timedelta(n) + +def get_number_of_leave_days(from_date, to_date, holiday_list): + number_of_days = date_diff(to_date, from_date) + 1 + + holidays = frappe.db.sql(""" + SELECT + COUNT(DISTINCT holiday_date) + FROM `tabHoliday` h1,`tabHoliday List` h2 + WHERE + h1.parent = h2.name and + h1.holiday_date between %s and %s and + h2.name = %s""", (from_date, to_date, holiday_list))[0][0] + + number_of_days = flt(number_of_days) - flt(holidays) + + return number_of_days diff --git a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py index e9b568ad70..fcdd42825f 100644 --- a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py @@ -5,13 +5,15 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import getdate, add_days +from frappe.utils import getdate, add_days, add_months +from erpnext import get_default_company from erpnext.education.doctype.student_group.test_student_group import get_random_group from erpnext.education.doctype.student.test_student import create_student class TestStudentLeaveApplication(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabStudent Leave Application`""") + create_holiday_list() def test_attendance_record_creation(self): leave_application = create_leave_application() @@ -35,20 +37,45 @@ class TestStudentLeaveApplication(unittest.TestCase): attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus') self.assertTrue(attendance_status, 2) + def test_holiday(self): + today = getdate() + leave_application = create_leave_application(from_date=today, to_date= add_days(today, 1), submit=0) -def create_leave_application(from_date=None, to_date=None, mark_as_present=0): + # holiday list validation + company = get_default_company() or frappe.get_all('Company')[0].name + frappe.db.set_value('Company', company, 'default_holiday_list', '') + self.assertRaises(frappe.ValidationError, leave_application.save) + + frappe.db.set_value('Company', company, 'default_holiday_list', 'Test Holiday List for Student') + leave_application.save() + + leave_application.reload() + self.assertEqual(leave_application.total_leave_days, 1) + + # check no attendance record created for a holiday + leave_application.submit() + self.assertIsNone(frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'date': add_days(today, 1)})) + + def tearDown(self): + company = get_default_company() or frappe.get_all('Company')[0].name + frappe.db.set_value('Company', company, 'default_holiday_list', '_Test Holiday List') + + +def create_leave_application(from_date=None, to_date=None, mark_as_present=0, submit=1): student = get_student() - leave_application = frappe.get_doc({ - 'doctype': 'Student Leave Application', - 'student': student.name, - 'attendance_based_on': 'Student Group', - 'student_group': get_random_group().name, - 'from_date': from_date if from_date else getdate(), - 'to_date': from_date if from_date else getdate(), - 'mark_as_present': mark_as_present - }).insert() - leave_application.submit() + leave_application = frappe.new_doc('Student Leave Application') + leave_application.student = student.name + leave_application.attendance_based_on = 'Student Group' + leave_application.student_group = get_random_group().name + leave_application.from_date = from_date if from_date else getdate() + leave_application.to_date = from_date if from_date else getdate() + leave_application.mark_as_present = mark_as_present + + if submit: + leave_application.insert() + leave_application.submit() + return leave_application def create_student_attendance(date=None, status=None): @@ -67,4 +94,22 @@ def get_student(): email='test_student@gmail.com', first_name='Test', last_name='Student' - )) \ No newline at end of file + )) + +def create_holiday_list(): + holiday_list = 'Test Holiday List for Student' + today = getdate() + if not frappe.db.exists('Holiday List', holiday_list): + frappe.get_doc(dict( + doctype = 'Holiday List', + holiday_list_name = holiday_list, + from_date = add_months(today, -6), + to_date = add_months(today, 6), + holidays = [ + dict(holiday_date=add_days(today, 1), description = 'Test') + ] + )).insert() + + company = get_default_company() or frappe.get_all('Company')[0].name + frappe.db.set_value('Company', company, 'default_holiday_list', holiday_list) + return holiday_list \ No newline at end of file diff --git a/erpnext/education/report/absent_student_report/absent_student_report.py b/erpnext/education/report/absent_student_report/absent_student_report.py index 4e57cc6c22..c3487ccaff 100644 --- a/erpnext/education/report/absent_student_report/absent_student_report.py +++ b/erpnext/education/report/absent_student_report/absent_student_report.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cstr, cint, getdate +from frappe.utils import formatdate from frappe import msgprint, _ +from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday def execute(filters=None): if not filters: filters = {} @@ -15,6 +17,11 @@ def execute(filters=None): columns = get_columns(filters) date = filters.get("date") + holiday_list = get_holiday_list() + if is_holiday(holiday_list, filters.get("date")): + msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date"))))) + + absent_students = get_absent_students(date) leave_applicants = get_leave_applications(date) if absent_students: diff --git a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py index c65d233ccc..7793dcf395 100644 --- a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py +++ b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cstr, cint, getdate +from frappe.utils import formatdate from frappe import msgprint, _ +from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday def execute(filters=None): if not filters: filters = {} @@ -12,6 +14,10 @@ def execute(filters=None): if not filters.get("date"): msgprint(_("Please select date"), raise_exception=1) + holiday_list = get_holiday_list() + if is_holiday(holiday_list, filters.get("date")): + msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date"))))) + columns = get_columns(filters) active_student_group = get_active_student_group() diff --git a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py index d820bfbb21..04dc8c0e56 100644 --- a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py +++ b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py @@ -7,6 +7,8 @@ from frappe.utils import cstr, cint, getdate, get_first_day, get_last_day, date_ from frappe import msgprint, _ from calendar import monthrange from erpnext.education.api import get_student_group_students +from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list +from erpnext.support.doctype.issue.issue import get_holidays def execute(filters=None): if not filters: filters = {} @@ -19,26 +21,32 @@ def execute(filters=None): students_list = get_students_list(students) att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list) data = [] + for stud in students: row = [stud.student, stud.student_name] student_status = frappe.db.get_value("Student", stud.student, "enabled") date = from_date total_p = total_a = 0.0 + for day in range(total_days_in_month): status="None" + if att_map.get(stud.student): status = att_map.get(stud.student).get(date, "None") elif not student_status: status = "Inactive" else: status = "None" - status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-"} + + status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-", "Holiday":"H"} row.append(status_map[status]) + if status == "Present": total_p += 1 elif status == "Absent": total_a += 1 date = add_days(date, 1) + row += [total_p, total_a] data.append(row) return columns, data @@ -63,14 +71,19 @@ def get_attendance_list(from_date, to_date, student_group, students_list): and date between %s and %s order by student, date''', (student_group, from_date, to_date), as_dict=1) + att_map = {} students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list) for d in attendance_list: att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "") + if students_with_leave_application.get(d.date) and d.student in students_with_leave_application.get(d.date): att_map[d.student][d.date] = "Present" else: att_map[d.student][d.date] = d.status + + att_map = mark_holidays(att_map, from_date, to_date, students_list) + return att_map def get_students_with_leave_application(from_date, to_date, students_list): @@ -108,3 +121,14 @@ def get_attendance_years(): if not year_list: year_list = [getdate().year] return "\n".join(str(year) for year in year_list) + +def mark_holidays(att_map, from_date, to_date, students_list): + holiday_list = get_holiday_list() + holidays = get_holidays(holiday_list) + + for dt in daterange(getdate(from_date), getdate(to_date)): + if dt in holidays: + for student in students_list: + att_map.setdefault(student, frappe._dict()).setdefault(dt, "Holiday") + + return att_map From d038a80c43ef8e0c345f5d9fc77ce38c8201cdd4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 9 Nov 2020 19:10:43 +0530 Subject: [PATCH 130/229] chore: Make eslint stricter (#23869) Error for - Invalid tab style - No space between unary operations - Space between blocks Warn for - Comma Spacing - Block Spacing - Object Key Spacing - Keyword Spacing --- .eslintrc | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index 757aa3caaf..d6f0f49363 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": 9, "sourceType": "module" }, "extends": "eslint:recommended", @@ -15,6 +15,14 @@ "tab", { "SwitchCase": 1 } ], + "brace-style": [ + "error", + "1tbs" + ], + "space-unary-ops": [ + "error", + { "words": true } + ], "linebreak-style": [ "error", "unix" @@ -44,12 +52,10 @@ "no-control-regex": [ "off" ], - "spaced-comment": [ - "warn" - ], - "no-trailing-spaces": [ - "warn" - ] + "space-before-blocks": "warn", + "keyword-spacing": "warn", + "comma-spacing": "warn", + "key-spacing": "warn" }, "root": true, "globals": { From bc64f5dfbbf497ba20de47bcfcc6cb84d38bfb18 Mon Sep 17 00:00:00 2001 From: UrvashiKishnani <41088003+UrvashiKishnani@users.noreply.github.com> Date: Mon, 9 Nov 2020 18:44:06 +0400 Subject: [PATCH 131/229] fix: spelling error in filter label (#23867) --- erpnext/public/js/salary_slip_deductions_report_filters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/salary_slip_deductions_report_filters.js b/erpnext/public/js/salary_slip_deductions_report_filters.js index 2b30e65075..1ca36600c3 100644 --- a/erpnext/public/js/salary_slip_deductions_report_filters.js +++ b/erpnext/public/js/salary_slip_deductions_report_filters.js @@ -45,7 +45,7 @@ erpnext.salary_slip_deductions_report_filters = { }, { fieldname: "branch", - label: __("Barnch"), + label: __("Branch"), fieldtype: "Link", options: "Branch", } @@ -63,4 +63,4 @@ erpnext.salary_slip_deductions_report_filters = { } }); } -} \ No newline at end of file +} From 8cfb210dc4ced9153a352896b726debf7b476159 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Nov 2020 20:14:53 +0530 Subject: [PATCH 132/229] fix: Ignore doctypes on company transaction deleted (#23864) --- erpnext/setup/doctype/company/delete_company_transactions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index c94831ef93..566f20cfa1 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -27,7 +27,8 @@ def delete_company_transactions(company_name): if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", - "Company", "Bank Account"): + "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", + "Item Default"): delete_for_doctype(doctype, company_name) # reset company values From f38a1c305eb184ea5b00a727760fdac2dca83e8d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Nov 2020 20:16:28 +0530 Subject: [PATCH 133/229] refactor: show form buttons only if permissions exist (#23851) * refactor: show form buttons only if permissions exist * style: add semicolon * fix: typo in conditions --- erpnext/setup/doctype/company/company.js | 48 +++++++++++++++--------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index f882db60c5..cbf67b4cd6 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -90,29 +90,41 @@ frappe.ui.form.on("Company", { frm.toggle_enable("default_currency", (frm.doc.__onload && !frm.doc.__onload.transactions_exist)); - frm.add_custom_button(__('Create Tax Template'), function() { - frm.trigger("make_default_tax_template"); - }); + if (frm.has_perm('write')) { + frm.add_custom_button(__('Create Tax Template'), function() { + frm.trigger("make_default_tax_template"); + }); + } - frm.add_custom_button(__('Cost Centers'), function() { - frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}) - }, __("View")); + if (frappe.perm.has_perm("Cost Center", 0, 'read')) { + frm.add_custom_button(__('Cost Centers'), function() { + frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Chart of Accounts'), function() { - frappe.set_route('Tree', 'Account', {'company': frm.doc.name}) - }, __("View")); + if (frappe.perm.has_perm("Account", 0, 'read')) { + frm.add_custom_button(__('Chart of Accounts'), function() { + frappe.set_route('Tree', 'Account', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Sales Tax Template'), function() { - frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name}); - }, __("View")); + if (frappe.perm.has_perm("Sales Taxes and Charges Template", 0, 'read')) { + frm.add_custom_button(__('Sales Tax Template'), function() { + frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Purchase Tax Template'), function() { - frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name}); - }, __("View")); + if (frappe.perm.has_perm("Purchase Taxes and Charges Template", 0, 'read')) { + frm.add_custom_button(__('Purchase Tax Template'), function() { + frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name}); + }, __("View")); + } - frm.add_custom_button(__('Default Tax Template'), function() { - frm.trigger("make_default_tax_template"); - }, __('Create')); + if (frm.has_perm('write')) { + frm.add_custom_button(__('Default Tax Template'), function() { + frm.trigger("make_default_tax_template"); + }, __('Create')); + } } erpnext.company.set_chart_of_accounts_options(frm.doc); From 4f880e74547348b9e918779b8c7310cefcae8bec Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 9 Nov 2020 15:47:56 +0100 Subject: [PATCH 134/229] refactor: zip_and_download (#23844) --- .../regional/germany/utils/datev/datev_csv.py | 16 +++++++++------- erpnext/regional/report/datev/datev.py | 8 +++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index fb7ca71e79..cf07a1c824 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -155,20 +155,22 @@ def get_header(filters, csv_class): return header -def download_csv_files_as_zip(csv_data_list): +def zip_and_download(zip_filename, csv_files): """ Put CSV files in a zip archive and send that to the client. Params: - csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}] + zip_filename Name of the zip file + csv_files list of dicts [{'file_name': 'my_file.csv', 'csv_data': 'comma,separated,values'}] """ zip_buffer = BytesIO() - datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) - for csv_file in csv_data_list: - datev_zip.writestr(csv_file.get('file_name'), csv_file.get('csv_data')) - datev_zip.close() + zip_file = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + for csv_file in csv_files: + zip_file.writestr(csv_file.get('file_name'), csv_file.get('csv_data')) + + zip_file.close() frappe.response['filecontent'] = zip_buffer.getvalue() - frappe.response['filename'] = 'DATEV.zip' + frappe.response['filename'] = zip_filename frappe.response['type'] = 'binary' diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 738806321c..dbae230f1e 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -11,10 +11,11 @@ from __future__ import unicode_literals import json import frappe -from frappe import _ from six import string_types + +from frappe import _ from erpnext.accounts.utils import get_fiscal_year -from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv +from erpnext.regional.germany.utils.datev.datev_csv import zip_and_download, get_datev_csv from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames COLUMNS = [ @@ -344,7 +345,8 @@ def download_datev_csv(filters): customers = get_customers(filters) suppliers = get_suppliers(filters) - download_csv_files_as_zip([ + zip_name = '{} DATEV.zip'.format(frappe.utils.datetime.date.today()) + zip_and_download(zip_name, [ { 'file_name': 'EXTF_Buchungsstapel.csv', 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions) From 91fdb7aa694f890896586f95821b3f318fc6b612 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Mon, 9 Nov 2020 20:20:05 +0530 Subject: [PATCH 135/229] feat: added column cost_center to receivable reports (#23835) --- .../report/accounts_receivable/accounts_receivable.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 044fc1d3ab..51fc7ec49a 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -160,6 +160,8 @@ class ReceivablePayableReport(object): else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + if gle.cost_center: + row.cost_center = str(gle.cost_center) def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -210,7 +212,6 @@ class ReceivablePayableReport(object): for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: # non-zero oustanding, we must consider this row @@ -577,7 +578,7 @@ class ReceivablePayableReport(object): self.gl_entries = frappe.db.sql(""" select - name, posting_date, account, party_type, party, voucher_type, voucher_no, + name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, against_voucher_type, against_voucher, account_currency, remarks, {0} from `tabGL Entry` @@ -741,6 +742,7 @@ class ReceivablePayableReport(object): self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', fieldtype='Link', options='Contact') + self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data') self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', options='voucher_type', width=180) From 57ce0462d9a17b74c4aeb6e2d880e46e2ada6b5b Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 9 Nov 2020 19:04:46 +0100 Subject: [PATCH 136/229] fix: remove check for exempt_from_sales_tax --- erpnext/regional/germany/accounts_controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 193c8e14a3..5b2b31f204 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -15,8 +15,7 @@ REQUIRED_FIELDS = { }, { "field_name": "taxes", - "regulation": "§ 14 Abs. 4 Nr. 8 UStG", - "condition": "not exempt_from_sales_tax" + "regulation": "§ 14 Abs. 4 Nr. 8 UStG" }, { "field_name": "customer_address", From 1924019531a6ed01d1140b83bd9fc094c114f047 Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 10 Nov 2020 13:19:21 +0530 Subject: [PATCH 137/229] fix: refactor --- erpnext/controllers/selling_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e5405b2e43..2a5617c168 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -386,14 +386,12 @@ class SellingController(StockController): po_nos = [] self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set((x.strip() for x in ','.join(po_nos).split(','))))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) if doc_list: - po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) - if po_no_list and po_no_list[0].get('po_no'): - po_nos += [d.po_no for d in po_no_list if d.po_no] + po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: From 1e564bc02d4ce292a913865bb7ab434e2042476d Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 10 Nov 2020 13:23:59 +0530 Subject: [PATCH 138/229] fix: refactor --- erpnext/controllers/selling_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 2a5617c168..7504746e07 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -380,13 +380,13 @@ class SellingController(StockController): po_nos = [] self.get_po_nos('Sales Order', 'sales_order', po_nos) self.get_po_nos('Delivery Note', 'delivery_note', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def set_pos_for_delivery_note(self): po_nos = [] self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set((x.strip() for x in ','.join(po_nos).split(','))))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) From 101bad3ea1d53371a5e1f24559feddb6c70988b5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Nov 2020 17:53:50 +0530 Subject: [PATCH 139/229] fix: Remarks for penalty GL Entry --- .../doctype/loan_repayment/loan_repayment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index a8887d7b24..415ba993c7 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -216,7 +216,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Repayment against loan:") + self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -232,7 +232,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Repayment against loan:") + self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) @@ -247,7 +247,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against Loan: ") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) @@ -263,7 +263,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against Loan: ") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) From d08c91673af3c21f77b402e6d1ee53bf9548344a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 10 Nov 2020 18:04:30 +0530 Subject: [PATCH 140/229] fix: removed unnecessary filter options --- .../doctype/production_plan/production_plan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 63df5f379d..7daf7069f3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -308,14 +308,14 @@ "fieldname": "sales_order_status", "fieldtype": "Select", "label": "Sales Order Status", - "options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed" + "options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-03 15:19:18.270529", + "modified": "2020-11-10 18:01:54.991970", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", From b648b9cf93dcbc47a5f33fa49acbc601e14ff614 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 10 Nov 2020 18:50:02 +0530 Subject: [PATCH 141/229] fix: code review changes --- erpnext/public/js/controllers/accounts.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 1bceec0547..9c746ab50b 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -154,11 +154,8 @@ cur_frm.cscript.account_head = function(doc, cdt, cdn) { callback: function(r) { if(d.charge_type!=="Actual"){ frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0); - frappe.model.set_value(cdt, cdn, "description", r.message.account_name); - } - else if(d.charge_type == 'Actual'){ - frappe.model.set_value(cdt, cdn, "description", r.message.account_name); } + frappe.model.set_value(cdt, cdn, "description", r.message.account_name); } }) } From 7e4d115e36d9026f8fe5ad60c6d80e56c19c826b Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 11 Nov 2020 09:52:27 +0530 Subject: [PATCH 142/229] fix: sider issues fixed --- erpnext/public/js/controllers/accounts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 9c746ab50b..29f35958e1 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -146,13 +146,13 @@ cur_frm.cscript.account_head = function(doc, cdt, cdn) { if(!d.charge_type && d.account_head){ frappe.msgprint(__("Please select Charge Type first")); frappe.model.set_value(cdt, cdn, "account_head", ""); - } else if(d.account_head) { + } else if (d.account_head) { frappe.call({ type:"GET", method: "erpnext.controllers.accounts_controller.get_tax_rate", args: {"account_head":d.account_head}, callback: function(r) { - if(d.charge_type!=="Actual"){ + if (d.charge_type!=="Actual") { frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0); } frappe.model.set_value(cdt, cdn, "description", r.message.account_name); From 780b6b466b138bb758d7aef00ea58c2a476f8e55 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 11 Nov 2020 11:44:12 +0530 Subject: [PATCH 143/229] fix(hooks): include js/css with full path --- erpnext/hooks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index dbb6c0d92e..90ae6442e4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -15,10 +15,10 @@ app_logo_url = '/assets/erpnext/images/erp-icon.svg' develop_version = '13.x.x-develop' -app_include_js = "assets/js/erpnext.min.js" -app_include_css = "assets/css/erpnext.css" -web_include_js = "assets/js/erpnext-web.min.js" -web_include_css = "assets/css/erpnext-web.css" +app_include_js = "/assets/js/erpnext.min.js" +app_include_css = "/assets/css/erpnext.css" +web_include_js = "/assets/js/erpnext-web.min.js" +web_include_css = "/assets/css/erpnext-web.css" doctype_js = { "Address": "public/js/address.js", From 6c95bc5e11dfe6e31ce2624520b30676b17428b2 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 11 Nov 2020 15:13:38 +0530 Subject: [PATCH 144/229] fix: payroll attendance error --- .../doctype/payroll_entry/payroll_entry.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 30ea432678..49c204ab44 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -344,9 +344,13 @@ class PayrollEntry(Document): employees_to_mark_attendance = [] days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0 for employee_detail in self.employees: - days_holiday = self.get_count_holidays_of_employee(employee_detail.employee) - days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee) - days_in_payroll = date_diff(self.end_date, self.start_date) + 1 + employee_joining_date = frappe.db.get_value("Employee", employee_detail.employee, 'date_of_joining') + start_date = self.start_date + if employee_joining_date > getdate(self.start_date): + start_date = employee_joining_date + days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date) + days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee, start_date) + days_in_payroll = date_diff(self.end_date, start_date) + 1 if days_in_payroll > days_holiday + days_attendance_marked: employees_to_mark_attendance.append({ "employee": employee_detail.employee, @@ -354,22 +358,22 @@ class PayrollEntry(Document): }) return employees_to_mark_attendance - def get_count_holidays_of_employee(self, employee): + def get_count_holidays_of_employee(self, employee, start_date): holiday_list = get_holiday_list_for_employee(employee) holidays = 0 if holiday_list: days = frappe.db.sql("""select count(*) from tabHoliday where parent=%s and holiday_date between %s and %s""", (holiday_list, - self.start_date, self.end_date)) + start_date, self.end_date)) if days and days[0][0]: holidays = days[0][0] return holidays - def get_count_employee_attendance(self, employee): + def get_count_employee_attendance(self, employee, start_date): marked_days = 0 attendances = frappe.db.sql("""select count(*) from tabAttendance where employee=%s and docstatus=1 and attendance_date between %s and %s""", - (employee, self.start_date, self.end_date)) + (employee, start_date, self.end_date)) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days From a91b68c8689073296815033d12a94063977de20b Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 11 Nov 2020 16:34:43 +0530 Subject: [PATCH 145/229] feat: add communication channel to communication medium (#23793) Co-authored-by: Saqib Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- .../communication_medium/communication_medium.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json index f009b38877..1e1fe3bf49 100644 --- a/erpnext/communication/doctype/communication_medium/communication_medium.json +++ b/erpnext/communication/doctype/communication_medium/communication_medium.json @@ -1,12 +1,14 @@ { + "actions": [], "autoname": "Prompt", "creation": "2019-06-05 11:48:30.572795", "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "communication_channel", "communication_medium_type", - "catch_all", "column_break_3", + "catch_all", "provider", "disabled", "timeslots_section", @@ -54,9 +56,16 @@ "fieldtype": "Table", "label": "Timeslots", "options": "Communication Medium Timeslot" + }, + { + "fieldname": "communication_channel", + "fieldtype": "Select", + "label": "Communication Channel", + "options": "\nExotel" } ], - "modified": "2019-06-05 11:49:30.769006", + "links": [], + "modified": "2020-10-27 16:22:08.068542", "modified_by": "Administrator", "module": "Communication", "name": "Communication Medium", From 0ea0a7495c5de280cfb97cb8de31012d0aef7081 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 12 Nov 2020 11:10:59 +0530 Subject: [PATCH 146/229] fix: making company address read-only in delivery note (#23890) --- erpnext/stock/doctype/delivery_note/delivery_note.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ea385c8b2a..3c5129b1ab 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -413,7 +413,8 @@ { "fieldname": "company_address_display", "fieldtype": "Small Text", - "label": "Company Address" + "label": "Company Address", + "read_only": 1 }, { "collapsible": 1, @@ -1255,7 +1256,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-08-03 23:18:47.739997", + "modified": "2020-11-11 14:57:16.388139", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From 2f5d896f8ce86a7f4127ae7fc7a3718a2526cc30 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 12 Nov 2020 16:57:42 +0530 Subject: [PATCH 147/229] fix: added wrong absent days calculation in salary slip --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 4ccf56435d..cecb8cde7c 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -240,7 +240,6 @@ class SalarySlip(TransactionBase): self.absent_days += unmarked_days #will be treated as absent self.payment_days -= unmarked_days if include_holidays_in_total_working_days: - self.absent_days -= len(holidays) for holiday in holidays: if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }): self.payment_days += 1 From 37dc369af9b3015bb93012bc0b94ef7c32e262f2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:53:35 +0530 Subject: [PATCH 148/229] feat: Sync old shopify orders (#23841) * feat: Sync old shopify orders * fix: Old orders syncing by date * fix: Remove unnecessary code * fix: Remove unintentional changes Co-authored-by: Saurabh --- .../connectors/shopify_connection.py | 97 ++++++++++++++++--- .../shopify_settings/shopify_settings.json | 78 ++++++++++++++- .../shopify_settings/shopify_settings.py | 12 ++- .../shopify_settings/test_shopify_settings.py | 4 +- erpnext/hooks.py | 1 + erpnext/patches.txt | 3 +- .../v13_0/update_custom_fields_for_shopify.py | 10 ++ 7 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 erpnext/patches/v13_0/update_custom_fields_for_shopify.py diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index d59f909298..8aa7453bd6 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -2,12 +2,13 @@ from __future__ import unicode_literals import frappe from frappe import _ import json -from frappe.utils import cstr, cint, nowdate, flt +from frappe.utils import cstr, cint, nowdate, getdate, flt, get_request_session, get_datetime from erpnext.erpnext_integrations.utils import validate_webhooks_request from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data +from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header @frappe.whitelist(allow_guest=True) @validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret') @@ -18,7 +19,7 @@ def store_request_data(order=None, event=None): dump_request_data(order, event) -def sync_sales_order(order, request_id=None): +def sync_sales_order(order, request_id=None, old_order_sync=False): frappe.set_user('Administrator') shopify_settings = frappe.get_doc("Shopify Settings") frappe.flags.request_id = request_id @@ -27,7 +28,7 @@ def sync_sales_order(order, request_id=None): try: validate_customer(order, shopify_settings) validate_item(order, shopify_settings) - create_order(order, shopify_settings) + create_order(order, shopify_settings, old_order_sync=old_order_sync) except Exception as e: make_shopify_log(status="Error", exception=e) @@ -77,13 +78,13 @@ def validate_item(order, shopify_settings): if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"): sync_item_from_shopify(shopify_settings, item) -def create_order(order, shopify_settings, company=None): +def create_order(order, shopify_settings, old_order_sync=False, company=None): so = create_sales_order(order, shopify_settings, company) if so: if order.get("financial_status") == "paid": - create_sales_invoice(order, shopify_settings, so) + create_sales_invoice(order, shopify_settings, so, old_order_sync=old_order_sync) - if order.get("fulfillments"): + if order.get("fulfillments") and not old_order_sync: create_delivery_note(order, shopify_settings, so) def create_sales_order(shopify_order, shopify_settings, company=None): @@ -92,7 +93,7 @@ def create_sales_order(shopify_order, shopify_settings, company=None): so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name") if not so: - items = get_order_items(shopify_order.get("line_items"), shopify_settings) + items = get_order_items(shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get('created_at'))) if not items: message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master' @@ -106,8 +107,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None): "doctype": "Sales Order", "naming_series": shopify_settings.sales_order_series or "SO-Shopify-", "shopify_order_id": shopify_order.get("id"), + "shopify_order_number": shopify_order.get("name"), "customer": customer or shopify_settings.default_customer, - "delivery_date": nowdate(), + "transaction_date": getdate(shopify_order.get("created_at")) or nowdate(), + "delivery_date": getdate(shopify_order.get("created_at")) or nowdate(), "company": shopify_settings.company, "selling_price_list": shopify_settings.price_list, "ignore_pricing_rule": 1, @@ -132,12 +135,20 @@ def create_sales_order(shopify_order, shopify_settings, company=None): frappe.db.commit() return so -def create_sales_invoice(shopify_order, shopify_settings, so): +def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=False): if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\ and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice): + if old_order_sync: + posting_date = getdate(shopify_order.get('created_at')) + else: + posting_date = nowdate() + si = make_sales_invoice(so.name, ignore_permissions=True) si.shopify_order_id = shopify_order.get("id") + si.shopify_order_number = shopify_order.get("name") + si.set_posting_time = 1 + si.posting_date = posting_date si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-" si.flags.ignore_mandatory = True set_cost_center(si.items, shopify_settings.cost_center) @@ -169,6 +180,9 @@ def create_delivery_note(shopify_order, shopify_settings, so): dn = make_delivery_note(so.name) dn.shopify_order_id = fulfillment.get("order_id") + dn.shopify_order_number = shopify_order.get("name") + dn.set_posting_time = 1 + dn.posting_date = getdate(fulfillment.get("created_at")) dn.shopify_fulfillment_id = fulfillment.get("id") dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-" dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings) @@ -187,7 +201,7 @@ def get_discounted_amount(order): discounted_amount += flt(discount.get("amount")) return discounted_amount -def get_order_items(order_items, shopify_settings): +def get_order_items(order_items, shopify_settings, delivery_date): items = [] all_product_exists = True product_not_exists = [] @@ -205,7 +219,7 @@ def get_order_items(order_items, shopify_settings): "item_code": item_code, "item_name": shopify_item.get("name"), "rate": shopify_item.get("price"), - "delivery_date": nowdate(), + "delivery_date": delivery_date, "qty": shopify_item.get("quantity"), "stock_uom": shopify_item.get("uom") or _("Nos"), "warehouse": shopify_settings.warehouse @@ -265,3 +279,64 @@ def get_tax_account_head(tax): frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title"))) return tax_account + +@frappe.whitelist(allow_guest=True) +def sync_old_orders(): + frappe.set_user('Administrator') + shopify_settings = frappe.get_doc('Shopify Settings') + + if not shopify_settings.sync_missing_orders: + return + + url = get_url(shopify_settings) + session = get_request_session() + + try: + res = session.get(url, headers=get_header(shopify_settings)) + res.raise_for_status() + orders = res.json()["orders"] + + for order in orders: + if is_sync_complete(shopify_settings, order): + stop_sync(shopify_settings) + return + + sync_sales_order(order=order, old_order_sync=True) + last_order_id = order.get('id') + + if last_order_id: + shopify_settings.load_from_db() + shopify_settings.last_order_id = last_order_id + shopify_settings.save() + frappe.db.commit() + + except Exception as e: + raise e + +def stop_sync(shopify_settings): + shopify_settings.sync_missing_orders = 0 + shopify_settings.last_order_id = '' + shopify_settings.save() + frappe.db.commit() + +def get_url(shopify_settings): + last_order_id = shopify_settings.last_order_id + + if not last_order_id: + if shopify_settings.sync_based_on == 'Date': + url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&created_at_min={0}&since_id=0".format( + get_datetime(shopify_settings.from_date)), shopify_settings) + else: + url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format( + shopify_settings.from_order_id), shopify_settings) + else: + url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings) + + return url + +def is_sync_complete(shopify_settings, order): + if shopify_settings.sync_based_on == 'Date': + return getdate(shopify_settings.to_date) < getdate(order.get('created_at')) + else: + return cstr(order.get('id')) == cstr(shopify_settings.to_order_id) + diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json index 2e10751f96..20ec06373e 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json @@ -1,7 +1,9 @@ { + "actions": [], "creation": "2015-05-18 05:21:07.270859", "doctype": "DocType", "document_type": "System", + "engine": "InnoDB", "field_order": [ "status_html", "enable_shopify", @@ -40,7 +42,16 @@ "sales_invoice_series", "section_break_22", "html_16", - "taxes" + "taxes", + "syncing_details_section", + "sync_missing_orders", + "sync_based_on", + "column_break_41", + "from_date", + "to_date", + "from_order_id", + "to_order_id", + "last_order_id" ], "fields": [ { @@ -255,10 +266,71 @@ "fieldtype": "Table", "label": "Shopify Tax Account", "options": "Shopify Tax Account" + }, + { + "collapsible": 1, + "fieldname": "syncing_details_section", + "fieldtype": "Section Break", + "label": "Syncing Missing Orders" + }, + { + "depends_on": "eval:doc.sync_missing_orders", + "fieldname": "last_order_id", + "fieldtype": "Data", + "label": "Last Order Id", + "read_only": 1 + }, + { + "fieldname": "column_break_41", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "On checking this Order from the ", + "fieldname": "sync_missing_orders", + "fieldtype": "Check", + "label": "Sync Missing Old Shopify Orders" + }, + { + "depends_on": "eval:doc.sync_missing_orders", + "fieldname": "sync_based_on", + "fieldtype": "Select", + "label": "Sync Based On", + "mandatory_depends_on": "eval:doc.sync_missing_orders", + "options": "\nDate\nShopify Order Id" + }, + { + "depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders" + }, + { + "depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders" + }, + { + "depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders", + "fieldname": "from_order_id", + "fieldtype": "Data", + "label": "From Order Id", + "mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders" + }, + { + "depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders", + "fieldname": "to_order_id", + "fieldtype": "Data", + "label": "To Order Id", + "mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders" } ], "issingle": 1, - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2020-11-05 20:44:03.664891", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Shopify Settings", @@ -277,4 +349,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py index 25ffd28109..cbdf90681d 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py @@ -87,7 +87,7 @@ def get_shopify_url(path, settings): def get_header(settings): header = {'Content-Type': 'application/json'} - return header; + return header @frappe.whitelist() def get_series(): @@ -121,17 +121,23 @@ def setup_custom_fields(): ], "Sales Order": [ dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1) + fieldtype='Data', insert_after='title', read_only=1, print_hide=1), + dict(fieldname='shopify_order_number', label='Shopify Order Number', + fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1) ], "Delivery Note":[ dict(fieldname='shopify_order_id', label='Shopify Order Id', fieldtype='Data', insert_after='title', read_only=1, print_hide=1), + dict(fieldname='shopify_order_number', label='Shopify Order Number', + fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1), dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id', fieldtype='Data', insert_after='title', read_only=1, print_hide=1) ], "Sales Invoice": [ dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1) + fieldtype='Data', insert_after='title', read_only=1, print_hide=1), + dict(fieldname='shopify_order_number', label='Shopify Order Number', + fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1) ] } diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 64ef3dc085..30fa23cfb4 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -58,7 +58,7 @@ class ShopifySettings(unittest.TestCase): }).save(ignore_permissions=True) self.shopify_settings = shopify_settings - + def test_order(self): ### Create Customer ### with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer: @@ -75,7 +75,7 @@ class ShopifySettings(unittest.TestCase): with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order: shopify_order = json.load(shopify_order) - create_order(shopify_order.get("order"), self.shopify_settings, "_Test Company") + create_order(shopify_order.get("order"), self.shopify_settings, False, company="_Test Company") sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))}) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 90ae6442e4..78ef66585f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -307,6 +307,7 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", + "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 34dbdd0bd5..25be884117 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -733,4 +733,5 @@ erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee -execute:frappe.delete_doc("Report", "Quoted Item Comparison") \ No newline at end of file +erpnext.patches.v13_0.update_custom_fields_for_shopify +execute:frappe.delete_doc("Report", "Quoted Item Comparison") diff --git a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py new file mode 100644 index 0000000000..f1d2ea2d74 --- /dev/null +++ b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py @@ -0,0 +1,10 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import setup_custom_fields + +def execute(): + if frappe.db.get_single_value('Shopify Settings', 'enable_shopify'): + setup_custom_fields() From 1a4cafb97d0d6ab63033d18292cedfffaa747130 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 13 Nov 2020 17:04:05 +0530 Subject: [PATCH 149/229] fix: change query to frappe.get_all --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 49c204ab44..a3d12c35c0 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -371,9 +371,12 @@ class PayrollEntry(Document): def get_count_employee_attendance(self, employee, start_date): marked_days = 0 - attendances = frappe.db.sql("""select count(*) from tabAttendance where - employee=%s and docstatus=1 and attendance_date between %s and %s""", - (employee, start_date, self.end_date)) + attendances = frappe.get_all("Attendance", + fields = ["count(*)"], + filters = { + "employee": employee, + "attendance_date": ('between', [start_date, self.end_date]) + }, as_list=1) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days From 4abc5657ab352d0d2fd91fbd86fe430ba26deff2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 13 Nov 2020 22:17:07 +0530 Subject: [PATCH 150/229] fix: not able to save bom --- erpnext/manufacturing/doctype/bom/bom.py | 31 +++++++++--------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 2ab1b98707..8888a96768 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -76,6 +76,7 @@ class BOM(WebsiteGenerator): self.set_routing_operations() self.validate_operations() self.calculate_cost() + self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, save=False) def get_context(self, context): @@ -84,8 +85,6 @@ class BOM(WebsiteGenerator): def on_update(self): frappe.cache().hdel('bom_children', self.name) self.check_recursion() - self.update_stock_qty() - self.update_exploded_items() def on_submit(self): self.manage_default_bom() @@ -237,7 +236,8 @@ class BOM(WebsiteGenerator): self.calculate_cost() if save: self.db_update() - self.update_exploded_items() + + self.update_exploded_items(save=save) # update parent BOMs if self.total_cost != existing_bom_cost and update_parent: @@ -318,8 +318,6 @@ class BOM(WebsiteGenerator): m.uom = m.stock_uom m.qty = m.stock_qty - m.db_update() - def validate_uom_is_interger(self): from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty", "BOM Item") @@ -372,15 +370,6 @@ class BOM(WebsiteGenerator): if raise_exception: frappe.throw(_("BOM recursion: {0} cannot be parent or child of {1}").format(self.name, self.name)) - def update_cost_and_exploded_items(self, bom_list=[]): - bom_list = self.traverse_tree(bom_list) - for bom in bom_list: - bom_obj = frappe.get_doc("BOM", bom) - bom_obj.check_recursion(bom_list=bom_list) - bom_obj.update_exploded_items() - - return bom_list - def traverse_tree(self, bom_list=None): def _get_children(bom_no): children = frappe.cache().hget('bom_children', bom_no) @@ -472,10 +461,10 @@ class BOM(WebsiteGenerator): d.rate = rate d.amount = (d.stock_qty or d.qty) * rate - def update_exploded_items(self): + def update_exploded_items(self, save=True): """ Update Flat BOM, following will be correct data""" self.get_exploded_items() - self.add_exploded_items() + self.add_exploded_items(save=save) def get_exploded_items(self): """ Get all raw materials including items from child bom""" @@ -544,11 +533,13 @@ class BOM(WebsiteGenerator): 'sourced_by_supplier': d.get('sourced_by_supplier', 0) })) - def add_exploded_items(self): + def add_exploded_items(self, save=True): "Add items to Flat BOM table" - frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name) self.set('exploded_items', []) + if save: + frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name) + for d in sorted(self.cur_exploded_items, key=itemgetter(0)): ch = self.append('exploded_items', {}) for i in self.cur_exploded_items[d].keys(): @@ -556,7 +547,9 @@ class BOM(WebsiteGenerator): ch.amount = flt(ch.stock_qty) * flt(ch.rate) ch.qty_consumed_per_unit = flt(ch.stock_qty) / flt(self.quantity) ch.docstatus = self.docstatus - ch.db_insert() + + if save: + ch.db_insert() def validate_bom_links(self): if not self.is_active: From 188ecd7b39f1812f05c60f0e5c70453775b0411f Mon Sep 17 00:00:00 2001 From: Marica Date: Sun, 15 Nov 2020 09:17:42 +0530 Subject: [PATCH 151/229] chore: PO cleanup (#23774) * chore: Subcontracting section and items section enhancement - Set target warehouse moved to Items section - Added subcontraction label to supply RM section * chore: PO & PO Item Form Cleanup * chore: PO Get Items from cleanup * fix: Taxes and Charges field visibility * chore: Cleanup * fix: Translation styling --- .../doctype/purchase_order/purchase_order.js | 39 ++++++--- .../purchase_order/purchase_order.json | 81 ++++++++++++------- .../purchase_order_item.json | 56 ++++++------- 3 files changed, 106 insertions(+), 70 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2f52a9e035..47483c9d1c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -90,6 +90,11 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( this.frm.set_df_property("drop_ship", "hidden", !is_drop_ship); if(doc.docstatus == 1) { + this.frm.fields_dict.items_section.wrapper.addClass("hide-border"); + if(!this.frm.doc.set_warehouse) { + this.frm.fields_dict.items_section.wrapper.removeClass("hide-border"); + } + if(!in_list(["Closed", "Delivered"], doc.status)) { if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { this.frm.add_custom_button(__('Update Items'), () => { @@ -126,16 +131,25 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if(doc.status != "Closed") { if (doc.status != "On Hold") { if(flt(doc.per_received) < 100 && allow_receipt) { - cur_frm.add_custom_button(__('Receipt'), this.make_purchase_receipt, __('Create')); + cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) { cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer")); } } if(flt(doc.per_billed) < 100) - cur_frm.add_custom_button(__('Invoice'), + cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); + if(flt(doc.per_billed)==0 && doc.status != "Delivered") { + cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); + } + + if(flt(doc.per_billed)==0) { + this.frm.add_custom_button(__('Payment Request'), + function() { me.make_payment_request() }, __('Create')); + } + if(!doc.auto_repeat) { cur_frm.add_custom_button(__('Subscription'), function() { erpnext.utils.make_subscription(doc.doctype, doc.name) @@ -156,13 +170,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( }); } } - if(flt(doc.per_billed)==0) { - this.frm.add_custom_button(__('Payment Request'), - function() { me.make_payment_request() }, __('Create')); - } - if(flt(doc.per_billed)==0 && doc.status != "Delivered") { - cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); - } + cur_frm.page.set_inner_btn_group_as_primary(__('Create')); } } else if(doc.docstatus===0) { @@ -358,12 +366,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order", source_doctype: "Material Request", target: me.frm, - setters: {}, + setters: { + schedule_date: undefined, + status: undefined + }, get_query_filters: { material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], per_ordered: ["<", 99.99], + company: me.frm.doc.company } }) }, __("Get Items From")); @@ -375,16 +387,17 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( source_doctype: "Supplier Quotation", target: me.frm, setters: { - supplier: me.frm.doc.supplier + supplier: me.frm.doc.supplier, + valid_till: undefined }, get_query_filters: { docstatus: 1, - status: ["!=", "Stopped"], + status: ["not in", ["Stopped", "Expired"]], } }) }, __("Get Items From")); - this.frm.add_custom_button(__('Update rate as per last purchase'), + this.frm.add_custom_button(__('Update Rate as per Last Purchase'), function() { frappe.call({ "method": "get_last_purchase_rate", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 2747c7c54d..4b865a98e9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -30,8 +30,8 @@ "customer_contact_email", "section_addresses", "supplier_address", - "contact_person", "address_display", + "contact_person", "contact_display", "contact_mobile", "contact_email", @@ -49,12 +49,14 @@ "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", - "set_warehouse", - "col_break_warehouse", "is_subcontracted", + "col_break_warehouse", "supplier_warehouse", - "items_section", + "before_items_section", "scan_barcode", + "items_col_break", + "set_warehouse", + "items_section", "items", "sb_last_purchase", "total_qty", @@ -108,18 +110,13 @@ "payment_terms_template", "payment_schedule", "tracking_section", - "per_billed", + "status", "column_break_75", + "per_billed", "per_received", "terms_section_break", "tc_name", "terms", - "more_info", - "status", - "ref_sq", - "column_break_74", - "party_account_currency", - "inter_company_order_reference", "column_break5", "letter_head", "select_print_heading", @@ -131,7 +128,12 @@ "to_date", "column_break_97", "auto_repeat", - "update_auto_repeat_reference" + "update_auto_repeat_reference", + "more_info", + "ref_sq", + "column_break_74", + "party_account_currency", + "inter_company_order_reference" ], "fields": [ { @@ -313,34 +315,34 @@ { "fieldname": "supplier_address", "fieldtype": "Link", - "label": "Select Supplier Address", + "label": "Supplier Address", "options": "Address", "print_hide": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", - "label": "Contact Person", + "label": "Supplier Contact", "options": "Contact", "print_hide": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", - "label": "Address", + "label": "Supplier Address Details", "read_only": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "in_global_search": 1, - "label": "Contact", + "label": "Contact Name", "read_only": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", - "label": "Mobile No", + "label": "Contact Mobile No", "read_only": 1 }, { @@ -358,14 +360,14 @@ { "fieldname": "shipping_address", "fieldtype": "Link", - "label": "Select Shipping Address", + "label": "Company Shipping Address", "options": "Address", "print_hide": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", - "label": "Shipping Address", + "label": "Shipping Address Details", "print_hide": 1, "read_only": 1 }, @@ -433,7 +435,8 @@ }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Subcontracting" }, { "description": "Sets 'Warehouse' in each row of the Items table.", @@ -466,6 +469,7 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_border": 1, "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -598,7 +602,8 @@ }, { "fieldname": "section_break_52", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "taxes", @@ -626,10 +631,12 @@ { "fieldname": "totals", "fieldtype": "Section Break", + "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" }, { + "depends_on": "base_taxes_and_charges_added", "fieldname": "base_taxes_and_charges_added", "fieldtype": "Currency", "label": "Taxes and Charges Added (Company Currency)", @@ -640,6 +647,7 @@ "read_only": 1 }, { + "depends_on": "base_taxes_and_charges_deducted", "fieldname": "base_taxes_and_charges_deducted", "fieldtype": "Currency", "label": "Taxes and Charges Deducted (Company Currency)", @@ -650,6 +658,7 @@ "read_only": 1 }, { + "depends_on": "base_total_taxes_and_charges", "fieldname": "base_total_taxes_and_charges", "fieldtype": "Currency", "label": "Total Taxes and Charges (Company Currency)", @@ -665,6 +674,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "taxes_and_charges_added", "fieldname": "taxes_and_charges_added", "fieldtype": "Currency", "label": "Taxes and Charges Added", @@ -675,6 +685,7 @@ "read_only": 1 }, { + "depends_on": "taxes_and_charges_deducted", "fieldname": "taxes_and_charges_deducted", "fieldtype": "Currency", "label": "Taxes and Charges Deducted", @@ -685,6 +696,7 @@ "read_only": 1 }, { + "depends_on": "total_taxes_and_charges", "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", "label": "Total Taxes and Charges", @@ -694,7 +706,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "discount_amount", + "collapsible_depends_on": "apply_discount_on", "fieldname": "discount_section", "fieldtype": "Section Break", "label": "Additional Discount" @@ -734,7 +746,8 @@ }, { "fieldname": "totals_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Totals" }, { "fieldname": "base_grand_total", @@ -902,12 +915,12 @@ }, { "fieldname": "ref_sq", - "fieldtype": "Data", - "hidden": 1, - "label": "Ref SQ", + "fieldtype": "Link", + "label": "Supplier Quotation", "no_copy": 1, "oldfieldname": "ref_sq", "oldfieldtype": "Data", + "options": "Supplier Quotation", "print_hide": 1, "read_only": 1 }, @@ -1061,7 +1074,7 @@ "collapsible": 1, "fieldname": "tracking_section", "fieldtype": "Section Break", - "label": "Tracking" + "label": "Order Status" }, { "fieldname": "column_break_75", @@ -1070,21 +1083,29 @@ { "fieldname": "billing_address", "fieldtype": "Link", - "label": "Select Billing Address", + "label": "Company Billing Address", "options": "Address" }, { "fieldname": "billing_address_display", "fieldtype": "Small Text", - "label": "Billing Address", + "label": "Billing Address Details", "read_only": 1 + }, + { + "fieldname": "before_items_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "items_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-07 14:31:57.661221", + "modified": "2020-10-30 11:39:37.388249", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", 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 7a52c28a0e..10db240a44 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -24,6 +24,7 @@ "col_break2", "uom", "conversion_factor", + "stock_qty", "sec_break1", "price_list_rate", "discount_percentage", @@ -46,11 +47,8 @@ "column_break_32", "base_net_rate", "base_net_amount", - "billed_amt", "warehouse_and_reference", "warehouse", - "delivered_by_supplier", - "project", "material_request", "material_request_item", "sales_order", @@ -58,36 +56,37 @@ "supplier_quotation", "supplier_quotation_item", "col_break5", + "delivered_by_supplier", "against_blanket_order", "blanket_order", "blanket_order_rate", "item_group", "brand", - "bom", - "include_exploded_items", "section_break_56", - "stock_qty", - "column_break_60", "received_qty", "returned_qty", - "manufacture_details", - "manufacturer", - "column_break_14", - "manufacturer_part_no", - "more_info_section_break", - "is_fixed_asset", - "item_tax_rate", + "column_break_60", + "billed_amt", "accounting_details", "expense_account", - "column_break_68", + "manufacture_details", + "manufacturer", + "manufacturer_part_no", + "column_break_14", + "bom", + "include_exploded_items", "item_weight_details", "weight_per_unit", "total_weight", "column_break_40", "weight_uom", "accounting_dimensions_section", - "cost_center", + "project", "dimension_col_break", + "cost_center", + "more_info_section_break", + "is_fixed_asset", + "item_tax_rate", "section_break_72", "page_break" ], @@ -346,6 +345,7 @@ }, { "default": "0", + "depends_on": "is_free_item", "fieldname": "is_free_item", "fieldtype": "Check", "label": "Is Free Item", @@ -508,9 +508,10 @@ }, { "default": "0", + "depends_on": "delivered_by_supplier", "fieldname": "delivered_by_supplier", "fieldtype": "Check", - "label": "To be delivered to customer", + "label": "To be Delivered to Customer", "print_hide": 1, "read_only": 1 }, @@ -558,6 +559,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_subcontracted == 'Yes'", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", @@ -574,21 +576,21 @@ }, { "fieldname": "section_break_56", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Billed, Received & Returned" }, { "fieldname": "stock_qty", "fieldtype": "Float", - "label": "Qty as per Stock UOM", + "label": "Qty in Stock UOM", "no_copy": 1, - "oldfieldname": "stock_qty", - "oldfieldtype": "Currency", "print_hide": 1, "print_width": "100px", "read_only": 1, "width": "100px" }, { + "depends_on": "received_qty", "fieldname": "received_qty", "fieldtype": "Float", "label": "Received Qty", @@ -612,9 +614,10 @@ "fieldtype": "Column Break" }, { + "depends_on": "billed_amt", "fieldname": "billed_amt", "fieldtype": "Currency", - "label": "Billed Amt", + "label": "Billed Amount", "no_copy": 1, "options": "currency", "print_hide": 1, @@ -633,6 +636,7 @@ "report_hide": 1 }, { + "collapsible": 1, "fieldname": "accounting_details", "fieldtype": "Section Break", "label": "Accounting Details" @@ -644,10 +648,6 @@ "options": "Account", "print_hide": 1 }, - { - "fieldname": "column_break_68", - "fieldtype": "Column Break" - }, { "fieldname": "cost_center", "fieldtype": "Link", @@ -715,6 +715,7 @@ }, { "default": "0", + "depends_on": "is_fixed_asset", "fetch_from": "item_code.is_fixed_asset", "fieldname": "is_fixed_asset", "fieldtype": "Check", @@ -728,9 +729,10 @@ } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-04-21 11:55:58.643393", + "modified": "2020-10-30 11:59:47.670951", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 4bf09dd02bacd1a4f5eb44bc8bccf53a5c9e6652 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 15 Nov 2020 05:02:24 +0100 Subject: [PATCH 152/229] fix: fiscal year can be shorter than 12 months (#23838) * fix: replace unnecessary SQL query * feat: checkbox "Is Short Year" Also, set checkbox and dates only once. * fix: toggle no longer necessary Date fields use set_only_once. * feat: short years can be less than 12 months * fix: if short year, don't add 12 months to start date * test: short year * fix: move short fiscal year to test records --- .../doctype/fiscal_year/fiscal_year.js | 14 +- .../doctype/fiscal_year/fiscal_year.json | 435 +++++------------- .../doctype/fiscal_year/fiscal_year.py | 19 +- .../doctype/fiscal_year/test_fiscal_year.py | 1 + .../doctype/fiscal_year/test_records.json | 7 + 5 files changed, 131 insertions(+), 345 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js index 152e17dbc8..bc77dac1cd 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js @@ -9,11 +9,7 @@ frappe.ui.form.on('Fiscal Year', { } }, refresh: function (frm) { - let doc = frm.doc; - frm.toggle_enable('year_start_date', doc.__islocal); - frm.toggle_enable('year_end_date', doc.__islocal); - - if (!doc.__islocal && (doc.name != frappe.sys_defaults.fiscal_year)) { + if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) { frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm)); frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'")); } else { @@ -24,8 +20,10 @@ frappe.ui.form.on('Fiscal Year', { return frm.call('set_as_default'); }, year_start_date: function(frm) { - let year_end_date = - frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1); - frm.set_value("year_end_date", year_end_date); + if (!frm.doc.is_short_year) { + let year_end_date = + frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1); + frm.set_value("year_end_date", year_end_date); + } }, }); diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 4ca9f6b96f..5ab91f2506 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -1,347 +1,126 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:year", - "beta": 0, - "creation": "2013-01-22 16:50:25", - "custom": 0, - "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "field:year", + "creation": "2013-01-22 16:50:25", + "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "year", + "disabled", + "is_short_year", + "year_start_date", + "year_end_date", + "companies", + "auto_created" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "For e.g. 2012, 2012-13", - "fieldname": "year", - "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": "Year Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "year", - "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, - "unique": 0 - }, + "description": "For e.g. 2012, 2012-13", + "fieldname": "year", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Year Name", + "oldfieldname": "year", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "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_global_search": 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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "year_start_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": "Year Start Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "year_start_date", - "oldfieldtype": "Date", - "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, - "unique": 0 - }, + "fieldname": "year_start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Year Start Date", + "no_copy": 1, + "oldfieldname": "year_start_date", + "oldfieldtype": "Date", + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "year_end_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": "Year End Date", - "length": 0, - "no_copy": 1, - "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, - "unique": 0 - }, + "fieldname": "year_end_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Year End Date", + "no_copy": 1, + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "companies", - "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": "Companies", - "length": 0, - "no_copy": 0, - "options": "Fiscal Year Company", - "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": "companies", + "fieldtype": "Table", + "label": "Companies", + "options": "Fiscal Year Company" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "auto_created", - "fieldtype": "Check", - "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": "Auto Created", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 + "default": "0", + "fieldname": "auto_created", + "fieldtype": "Check", + "hidden": 1, + "label": "Auto Created", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "description": "Less than 12 months.", + "fieldname": "is_short_year", + "fieldtype": "Check", + "label": "Is Short Year", + "set_only_once": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-calendar", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-04-25 14:21:41.273354", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Fiscal Year", - "owner": "Administrator", + ], + "icon": "fa fa-calendar", + "idx": 1, + "links": [], + "modified": "2020-11-05 12:16:53.081573", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Fiscal Year", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System 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": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Sales User" + }, { - "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": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Purchase User" + }, { - "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": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Accounts User" + }, { - "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": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Stock User" + }, { - "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": 1, - "sort_field": "name", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "name", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index d80bc7fad1..da6a3fd2ef 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -36,6 +36,11 @@ class FiscalYear(Document): frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.")) def validate_dates(self): + if self.is_short_year: + # Fiscal Year can be shorter than one year, in some jurisdictions + # under certain circumstances. For example, in the USA and Germany. + return + if getdate(self.year_start_date) > getdate(self.year_end_date): frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), FiscalYearIncorrectDate) @@ -116,12 +121,8 @@ def auto_create_fiscal_year(): pass def get_from_and_to_date(fiscal_year): - from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date - from `tabFiscal Year` where name=%s""", (fiscal_year))[0] - - from_and_to_date = { - "from_date": from_and_to_date_tuple[0], - "to_date": from_and_to_date_tuple[1] - } - - return from_and_to_date + fields = [ + "year_start_date as from_date", + "year_end_date as to_date" + ] + return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1) diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index f7b7782766..cec4f4492d 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -11,6 +11,7 @@ test_records = frappe.get_test_records('Fiscal Year') test_ignore = ["Company"] class TestFiscalYear(unittest.TestCase): + def test_extra_year(self): if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"): frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000") diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json index d5723ca62b..44052535cb 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_records.json +++ b/erpnext/accounts/doctype/fiscal_year/test_records.json @@ -1,4 +1,11 @@ [ + { + "doctype": "Fiscal Year", + "year": "_Test Short Fiscal Year 2011", + "is_short_year": 1, + "year_end_date": "2011-04-01", + "year_start_date": "2011-12-31" + }, { "doctype": "Fiscal Year", "year": "_Test Fiscal Year 2012", From 8ff0d0cff60dd38a25341f83978807aaf0216167 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 15 Nov 2020 05:04:05 +0100 Subject: [PATCH 153/229] feat: make account number length configurable (#23845) --- .../datev_settings/datev_settings.json | 22 ++++++++++++++----- .../regional/germany/utils/datev/datev_csv.py | 2 +- erpnext/regional/report/datev/datev.py | 2 ++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index 39486dfc12..713e8e34ef 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "field:client", "creation": "2019-08-13 23:56:34.259906", "doctype": "DocType", @@ -6,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "client", + "account_number_length", "column_break_2", "client_number", "section_break_4", @@ -28,8 +30,8 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Client ID", - "reqd": 1, - "length": 5 + "length": 5, + "reqd": 1 }, { "fieldname": "consultant", @@ -43,8 +45,8 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Consultant ID", - "reqd": 1, - "length": 7 + "length": 7, + "reqd": 1 }, { "fieldname": "column_break_2", @@ -57,9 +59,17 @@ { "fieldname": "column_break_6", "fieldtype": "Column Break" + }, + { + "default": "4", + "fieldname": "account_number_length", + "fieldtype": "Int", + "label": "Account Number Length", + "reqd": 1 } ], - "modified": "2019-08-14 00:03:26.616460", + "links": [], + "modified": "2020-11-05 17:52:11.674329", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", @@ -104,4 +114,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index cf07a1c824..f138a807bc 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -106,7 +106,7 @@ def get_header(filters, csv_class): # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(filters.get('fiscal_year_start'), 'yyyyMMdd'), # N = Length of account numbers (Sachkontenlänge) - datev_settings.get('account_number_length', '4'), + str(filters.get('account_number_length', 4)), # O = Transaction batch start date (YYYYMMDD) frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', # P = Transaction batch end date (YYYYMMDD) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index dbae230f1e..3f4cb981cc 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -340,6 +340,8 @@ def download_datev_csv(filters): coa = frappe.get_value('Company', company, 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + filters['account_number_length'] = frappe.get_value('DATEV Settings', company, 'account_number_length') + transactions = get_transactions(filters) account_names = get_account_names(filters) customers = get_customers(filters) From 1c8c1d78e9f7c05adfd9bd8e5723fa36d8568f4d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 15 Nov 2020 09:40:44 +0530 Subject: [PATCH 154/229] fix: default cost center in item master not set in stock entry (#23877) Co-authored-by: Marica --- .../doctype/work_order/test_work_order.py | 5 ++++ .../stock/doctype/stock_entry/stock_entry.py | 5 ++-- erpnext/stock/get_item_details.py | 23 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 5f8a13428c..e53927918e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -443,6 +443,11 @@ class TestWorkOrder(unittest.TestCase): ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) self.assertEqual(len(ste1.items), 3) + def test_cost_center_for_manufacture(self): + wo_order = make_wo_order_test_record() + ste = make_stock_entry(wo_order.name, "Material Transfer for Manufacture", wo_order.qty) + self.assertEquals(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC") + def test_operation_time_with_batch_size(self): fg_item = "Test Batch Size Item For BOM" rm1 = "Test Batch Size Item RM 1 For BOM" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 768526705c..e3159b95c3 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1227,8 +1227,6 @@ class StockEntry(StockController): return item_dict def add_to_stock_entry_detail(self, item_dict, bom_no=None): - cost_center = frappe.db.get_value("Company", self.company, 'cost_center') - for d in item_dict: stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") @@ -1239,9 +1237,10 @@ class StockEntry(StockController): se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom se_child.stock_uom = stock_uom se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty")) - se_child.cost_center = item_dict[d].get("cost_center") or cost_center se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0) se_child.subcontracted_item = item_dict[d].get("main_item_code") + se_child.cost_center = (item_dict[d].get("cost_center") or + get_default_cost_center(item_dict[d], company = self.company)) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name"]: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8d8dcb74c3..08f7a83b89 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -559,23 +559,40 @@ def get_default_deferred_account(args, item, fieldname=None): else: return None -def get_default_cost_center(args, item, item_group, brand, company=None): +def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None): cost_center = None + + if not company and args.get("company"): + company = args.get("company") + if args.get('project'): cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True) - if not cost_center: + if not cost_center and (item and item_group and brand): if args.get('customer'): cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center') else: cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center') - cost_center = cost_center or args.get("cost_center") + elif not cost_center and args.get("item_code") and company: + for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: + path = "erpnext.stock.get_item_details.{0}".format(method) + data = frappe.get_attr(path)(args.get("item_code"), company) + + if data and (data.selling_cost_center or data.buying_cost_center): + return data.selling_cost_center or data.buying_cost_center + + if not cost_center and args.get("cost_center"): + cost_center = args.get("cost_center") if (company and cost_center and frappe.get_cached_value("Cost Center", cost_center, "company") != company): return None + if not cost_center and company: + cost_center = frappe.get_cached_value("Company", + company, "cost_center") + return cost_center def get_default_supplier(args, item, item_group, brand): From c5966d3d70ae8f4a1596b127c60d36de5543ee87 Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> Date: Sun, 15 Nov 2020 09:44:36 +0530 Subject: [PATCH 155/229] fix: make contract template editable (#23891) --- erpnext/crm/doctype/contract_template/contract_template.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index ef9974f863..5e4582f8d3 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -23,8 +23,7 @@ { "fieldname": "contract_terms", "fieldtype": "Text Editor", - "label": "Contract Terms and Conditions", - "read_only": 1 + "label": "Contract Terms and Conditions" }, { "fieldname": "sb_fulfilment", @@ -45,7 +44,7 @@ } ], "links": [], - "modified": "2020-06-03 00:24:58.179816", + "modified": "2020-11-11 17:49:44.879363", "modified_by": "Administrator", "module": "CRM", "name": "Contract Template", From 7178df8567a633b45957ef2c4cee43c99829ee11 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 15 Nov 2020 05:15:10 +0100 Subject: [PATCH 156/229] fix: improve UX of DATEV report (#23892) * feat(DATEV Settings): button to show report * feat(desk): add DATEV to Reports under Accounting * fix(report DATEV): last calendar month as default * fix: let user create DATEV Settings (Instead of showing an error message.) --- .../desk_page/accounting/accounting.json | 4 ++-- .../doctype/datev_settings/datev_settings.js | 6 +++--- erpnext/regional/report/datev/datev.js | 18 ++++++++++++++++-- erpnext/regional/report/datev/datev.py | 19 +++++++++++++------ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 9172792411..85c9209205 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"DATEV Export\",\n \"name\": \"DATEV\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-11-06 13:05:58.650150", + "modified": "2020-11-11 18:35:11.542909", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.js b/erpnext/regional/doctype/datev_settings/datev_settings.js index 69747b0b89..f04705929f 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.js +++ b/erpnext/regional/doctype/datev_settings/datev_settings.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('DATEV Settings', { - // refresh: function(frm) { - - // } + refresh: function(frm) { + frm.add_custom_button('Show Report', () => frappe.set_route('query-report', 'DATEV'), "fa fa-table"); + } }); diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 55f12cf373..4124e3df19 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -11,14 +11,14 @@ frappe.query_reports["DATEV"] = { { "fieldname": "from_date", "label": __("From Date"), - "default": frappe.datetime.month_start(), + "default": moment().subtract(1, 'month').startOf('month').format(), "fieldtype": "Date", "reqd": 1 }, { "fieldname": "to_date", "label": __("To Date"), - "default": frappe.datetime.now_date(), + "default": moment().subtract(1, 'month').endOf('month').format(), "fieldtype": "Date", "reqd": 1 }, @@ -30,9 +30,23 @@ frappe.query_reports["DATEV"] = { } ], onload: function(query_report) { + let company = frappe.query_report.get_filter_value('company'); + frappe.db.exists('DATEV Settings', company).then((settings_exist) => { + if (!settings_exist) { + frappe.confirm(__('DATEV Settings for your Company are missing. Would you like to create them now?'), + () => frappe.new_doc('DATEV Settings', {'company': company}) + ); + } + }); + query_report.page.add_menu_item(__("Download DATEV File"), () => { const filters = JSON.stringify(query_report.get_values()); window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); }); + + query_report.page.add_menu_item(__("Change DATEV Settings"), () => { + let company = frappe.query_report.get_filter_value('company'); // read company from filters again – it might have changed by now. + frappe.set_route('Form', 'DATEV Settings', company); + }); } }; diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 3f4cb981cc..1e39c57786 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -94,8 +94,11 @@ COLUMNS = [ def execute(filters=None): """Entry point for frappe.""" - validate(filters) - return COLUMNS, get_transactions(filters, as_dict=0) + data = [] + if filters and validate(filters): + data = get_transactions(filters, as_dict=0) + + return COLUMNS, data def validate(filters): @@ -114,10 +117,14 @@ def validate(filters): validate_fiscal_year(from_date, to_date, company) - try: - frappe.get_doc('DATEV Settings', filters.get('company')) - except frappe.DoesNotExistError: - frappe.throw(_('Please create DATEV Settings for Company {}.').format(filters.get('company'))) + if not frappe.db.exists('DATEV Settings', filters.get('company')): + frappe.log_error(_('Please create {} for Company {}.').format( + '{}'.format(_('DATEV Settings')), + frappe.bold(filters.get('company')) + )) + return False + + return True def validate_fiscal_year(from_date, to_date, company): From 69be37f88d95d8be4ff64f468f0a7adfef59bd19 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Sun, 15 Nov 2020 11:14:35 +0530 Subject: [PATCH 157/229] fix: Error handling in Upload Attendance (#23907) --- erpnext/hr/doctype/upload_attendance/upload_attendance.js | 8 ++++---- erpnext/hr/doctype/upload_attendance/upload_attendance.py | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.js b/erpnext/hr/doctype/upload_attendance/upload_attendance.js index 9df2948a15..29aa85484a 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.js +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.js @@ -24,10 +24,10 @@ erpnext.hr.AttendanceControlPanel = frappe.ui.form.Controller.extend({ } window.location.href = repl(frappe.request.url + '?cmd=%(cmd)s&from_date=%(from_date)s&to_date=%(to_date)s', { - cmd: "erpnext.hr.doctype.upload_attendance.upload_attendance.get_template", - from_date: this.frm.doc.att_fr_date, - to_date: this.frm.doc.att_to_date, - }); + cmd: "erpnext.hr.doctype.upload_attendance.upload_attendance.get_template", + from_date: this.frm.doc.att_fr_date, + to_date: this.frm.doc.att_to_date, + }); }, show_upload() { diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index edf05e827b..674c8e3eb4 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -28,7 +28,12 @@ def get_template(): w = UnicodeWriter() w = add_header(w) - w = add_data(w, args) + try: + w = add_data(w, args) + except Exception as e: + frappe.clear_messages() + frappe.respond_as_web_page("Holiday List Missing", html=e) + return # write out response as a type csv frappe.response['result'] = cstr(w.getvalue()) From b8d0b546a7851fd24c9815a9e797a7717adab2b0 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 16 Nov 2020 12:58:38 +0530 Subject: [PATCH 158/229] feat(UAE VAT Format): use company address for emir --- erpnext/regional/united_arab_emirates/setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 62156e46be..6f520f6ad6 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -56,7 +56,8 @@ def make_custom_fields(): fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain'), + options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain', + fetch_from='company_address.emirate'), dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', insert_after='vat_emirate', fieldtype='Currency', print_hide=1, default='0'), ] @@ -100,6 +101,10 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Data', insert_after='supplier_name'), ], + 'Address': [ + dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', + options='Abu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') + ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields, 'Purchase Receipt': purchase_invoice_fields + invoice_fields, From 2e1787300d1b881d638e9786c52d6f43f85f780a Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 16 Nov 2020 17:36:27 +0530 Subject: [PATCH 159/229] feat(UAE VAT 201): emirate according to branch --- .../doctype/sales_invoice/sales_invoice.js | 23 +++++++++++++++++++ .../regional/united_arab_emirates/setup.py | 13 ++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 502e65ed8d..803312072c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -446,6 +446,29 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); this.frm.refresh_field("base_paid_amount"); + }, + + branch: function(){ + const me = this + if (this.frm.doc.branch) { + frappe.call({ + async: false, + method: "frappe.client.get_value", + args: { + "doctype": "Branch", + "filters": { + 'name': this.frm.doc.branch + }, + "fieldname": ['emirate'] + }, + callback: function (res) { + if (res.message && res.message['emirate']) { + me.frm.set_value("vat_emirate", res.message['emirate']) + me.frm.refresh_field("vat_emirate") + } + } + }) + } } }); diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 6f520f6ad6..2e546deb82 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -38,8 +38,9 @@ def make_custom_fields(): dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', fieldtype='Read Only', insert_after='supplier_name', fetch_from='supplier.supplier_name_in_arabic', print_hide=1), - dict(fieldname='recoverable_standard_rated_expenses', label='Recoverable Standard Rated Expenses (AED)', - insert_after='permit_no', fieldtype='Currency', print_hide=1, default='0'), + dict(fieldname='recoverable_standard_rated_expenses', print_hide=1, default='0', + label='Recoverable Standard Rated Expenses (AED)', insert_after='permit_no', + fieldtype='Currency', ), dict(fieldname='reverse_charge', label='Reverse Charge Applicable', fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1, options='Y\nN', default='N'), @@ -55,6 +56,8 @@ def make_custom_fields(): dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), + dict(fieldname='branch', label='Branch', options='Branch', + fieldtype='Link', insert_after='company', print_hide=1), dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain', fetch_from='company_address.emirate'), @@ -103,7 +106,11 @@ def make_custom_fields(): ], 'Address': [ dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', - options='Abu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') + options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') + ], + 'Branch': [ + dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', + options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields, From d2477e6cc5cd0acc9353e9dc8a0b23a729da6ff3 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Date: Mon, 16 Nov 2020 18:47:05 +0530 Subject: [PATCH 160/229] fix(UAE VAT 201): remove branch parts --- .../doctype/sales_invoice/sales_invoice.js | 23 ------------------- .../regional/united_arab_emirates/setup.py | 6 ----- 2 files changed, 29 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 803312072c..502e65ed8d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -446,29 +446,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); this.frm.refresh_field("base_paid_amount"); - }, - - branch: function(){ - const me = this - if (this.frm.doc.branch) { - frappe.call({ - async: false, - method: "frappe.client.get_value", - args: { - "doctype": "Branch", - "filters": { - 'name': this.frm.doc.branch - }, - "fieldname": ['emirate'] - }, - callback: function (res) { - if (res.message && res.message['emirate']) { - me.frm.set_value("vat_emirate", res.message['emirate']) - me.frm.refresh_field("vat_emirate") - } - } - }) - } } }); diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 2e546deb82..013ae5cf73 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -56,8 +56,6 @@ def make_custom_fields(): dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', fieldtype='Read Only', insert_after='customer_name', fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='branch', label='Branch', options='Branch', - fieldtype='Link', insert_after='company', print_hide=1), dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain', fetch_from='company_address.emirate'), @@ -108,10 +106,6 @@ def make_custom_fields(): dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') ], - 'Branch': [ - dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') - ], 'Purchase Invoice': purchase_invoice_fields + invoice_fields, 'Purchase Order': purchase_invoice_fields + invoice_fields, 'Purchase Receipt': purchase_invoice_fields + invoice_fields, From 642819b955205e9faa64ae1e72c697c2a7950305 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 17 Nov 2020 09:47:10 +0530 Subject: [PATCH 161/229] fix: place of supply change when address changes --- erpnext/public/js/utils.js | 15 +++++++++++++++ erpnext/regional/india/utils.py | 1 + erpnext/selling/sales_common.js | 1 + 3 files changed, 17 insertions(+) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index ea2093eee1..b4fe412fe9 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -304,6 +304,21 @@ $.extend(erpnext.utils, { } frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); + }, + + set_place_of_supply: function(frm){ + frappe.call({ + method: "erpnext.regional.india.utils.get_place_of_supply", + args: { + "party_details": frm.doc, + "doctype": frm.doc.doctype + }, + callback: function(r){ + if(r.message){ + frm.set_value("place_of_supply", r.message) + } + } + }) } }); diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index dd87f0f660..c774cb03be 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -135,6 +135,7 @@ def test_method(): '''test function''' return 'overridden' +@frappe.whitelist() def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): return diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 002cfe41e1..77bdf2912f 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -117,6 +117,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ customer_address: function() { erpnext.utils.get_address_display(this.frm, "customer_address"); erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name"); + erpnext.utils.set_place_of_supply(this.frm) }, shipping_address_name: function() { From 5e8b00207b248433861d10945ef91d0595403bd0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 17 Nov 2020 10:58:09 +0530 Subject: [PATCH 162/229] fix: Typo (Enchashment > Encashment) (#23919) --- erpnext/patches/v12_0/generate_leave_ledger_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 7afde373c3..fe072d7eb9 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -51,7 +51,7 @@ def generate_encashment_leave_ledger_entries(): for encashment in leave_encashments: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): - frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() + frappe.get_doc("Leave Encashment", encashment).create_leave_ledger_entry() def generate_expiry_allocation_ledger_entries(): ''' fix ledger entries for missing leave allocation transaction ''' From 23614b650e40e46c20952b64dd256cd0781a455a Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 17 Nov 2020 11:12:31 +0530 Subject: [PATCH 163/229] fix: Don't copy terms and discount from SO to PO (#23903) * fix: Dont copy terms, discount and required by from SO to PO * fix: Let delivery date and required by date get mapped --- .../selling/doctype/sales_order/sales_order.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ec1c82339b..04d85e575c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -844,7 +844,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "contact_email", "contact_person", "taxes_and_charges", - "shipping_address" + "shipping_address", + "terms" ], "validation": { "docstatus": ["=", 1] @@ -863,7 +864,10 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "field_no_map": [ "rate", "price_list_rate", - "item_tax_template" + "item_tax_template", + "discount_percentage", + "discount_amount", + "pricing_rules" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map @@ -917,7 +921,8 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "contact_email", "contact_person", "taxes_and_charges", - "shipping_address" + "shipping_address", + "terms" ], "validation": { "docstatus": ["=", 1] @@ -937,7 +942,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "rate", "price_list_rate", "item_tax_template", - "supplier" + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map From c72b69322361a3291947b346b4cdca11c4dca8a6 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 17 Nov 2020 12:08:31 +0530 Subject: [PATCH 164/229] fix: Handle the "no leave_allocation found" case (#23922) * fix: Handle the "no leave_allocation found" case * fix: format of error msg --- erpnext/hr/doctype/leave_encashment/leave_encashment.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 8913c648c5..c1dcc97b1a 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -32,7 +32,7 @@ class LeaveEncashment(Document): additional_salary.employee = self.employee earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") if not earning_component: - frappe.throw(_("Please set Earning Component for Leave type: {0}.".format(self.leave_type))) + frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type)) additional_salary.salary_component = earning_component additional_salary.payroll_date = self.encashment_date additional_salary.amount = self.encashment_amount @@ -98,7 +98,11 @@ class LeaveEncashment(Document): create_leave_ledger_entry(self, args, submit) # create reverse entry for expired leaves - to_date = self.get_leave_allocation().get('to_date') + leave_allocation = self.get_leave_allocation() + if not leave_allocation: + return + + to_date = leave_allocation.get('to_date') if to_date < getdate(nowdate()): args = frappe._dict( leaves=self.encashable_days, From 7aa60d1dea89419d3cc8db7340806a40281cb4ac Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 17 Nov 2020 12:10:27 +0530 Subject: [PATCH 165/229] fix: stock ageing report not working (#23923) --- erpnext/stock/report/stock_ageing/stock_ageing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 3dc806fb43..8aaf7abcbe 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -20,7 +20,8 @@ def execute(filters=None): fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) details = item_dict["details"] - if not fifo_queue and (not item_dict.get("total_qty")): continue + + if not fifo_queue: continue average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) From a77f2ba0def67756a4f3b91fe02847615df39e20 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 17 Nov 2020 15:34:05 +0530 Subject: [PATCH 166/229] fix: validation for membership --- erpnext/non_profit/doctype/membership/membership.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 4c85cb60e8..7d15abaa3b 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -70,7 +70,7 @@ class Membership(Document): settings = frappe.get_doc("Membership Settings") if not member.customer: - frappe.throw(_("No customer linked to member {}", [member.name])) + frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) if not settings.debit_account: frappe.throw(_("You need to set Debit Account in Membership Settings")) From 327143731465510840c3b8c6fe3e12187807ca03 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 17 Nov 2020 17:52:59 +0530 Subject: [PATCH 167/229] fix: Handle for custom field IFSC code in Bank remittance report. (#23905) * fix: Handel for custom field IFSC code * fix: changes_requested --- .../report/bank_remittance/bank_remittance.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py index 4b052bf5c4..500543ceb0 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -47,33 +47,39 @@ def execute(filters=None): "fieldtype": "Int", "fieldname": "employee_account_no", "width": 50 - }, - { + } + ] + + if frappe.db.has_column('Employee', 'ifsc_code'): + columns.append({ "label": _("IFSC Code"), "fieldtype": "Data", "fieldname": "bank_code", "width": 100 - }, - { - "label": _("Currency"), - "fieldtype": "Data", - "fieldname": "currency", - "width": 50 - }, - { - "label": _("Net Salary Amount"), - "fieldtype": "Currency", - "options": "currency", - "fieldname": "amount", - "width": 100 - } - ] + }) + + columns += [{ + "label": _("Currency"), + "fieldtype": "Data", + "fieldname": "currency", + "width": 50 + }, + { + "label": _("Net Salary Amount"), + "fieldtype": "Currency", + "options": "currency", + "fieldname": "amount", + "width": 100 + }] + data = [] accounts = get_bank_accounts() payroll_entries = get_payroll_entries(accounts, filters) salary_slips = get_salary_slips(payroll_entries) - get_emp_bank_ifsc_code(salary_slips) + + if frappe.db.has_column('Employee', 'ifsc_code'): + get_emp_bank_ifsc_code(salary_slips) for salary in salary_slips: if salary.bank_name and salary.bank_account_no and salary.debit_acc_no and salary.status in ["Submitted", "Paid"]: From 6b10d87d468acdf4e810d5a4b4da4b389def6a06 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 17 Nov 2020 20:34:51 +0530 Subject: [PATCH 168/229] fix: place of supply change on address change --- erpnext/public/js/controllers/buying.js | 1 + erpnext/public/js/utils.js | 28 ++++++++++++------------- erpnext/regional/india/utils.py | 3 +++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index cb76c87b62..cd5cc9282b 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -135,6 +135,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ supplier_address: function() { erpnext.utils.get_address_display(this.frm); erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address"); + erpnext.utils.set_place_of_supply(this.frm) }, buying_price_list: function() { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index b4fe412fe9..1555896eac 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -116,6 +116,19 @@ $.extend(erpnext.utils, { } }, + set_place_of_supply: function(frm){ + frappe.call({ + method: "erpnext.regional.india.utils.get_place_of_supply", + args: { + "party_details": frm.doc, + "doctype": frm.doc.doctype + }, + callback: function(r){ + frm.set_value("place_of_supply", r.message) + } + }) + }, + add_indicator_for_multicompany: function(frm, info) { frm.dashboard.stats_area.removeClass('hidden'); frm.dashboard.stats_area_row.addClass('flex'); @@ -304,21 +317,6 @@ $.extend(erpnext.utils, { } frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); - }, - - set_place_of_supply: function(frm){ - frappe.call({ - method: "erpnext.regional.india.utils.get_place_of_supply", - args: { - "party_details": frm.doc, - "doctype": frm.doc.doctype - }, - callback: function(r){ - if(r.message){ - frm.set_value("place_of_supply", r.message) - } - } - }) } }); diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index c774cb03be..7ad1c07f93 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -138,6 +138,9 @@ def test_method(): @frappe.whitelist() def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): 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", "Sales Order"): address_name = party_details.customer_address or party_details.shipping_address_name From 69232f8dd1a3061fa650f8cbac185d5933f0bf6b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 22:00:41 +0530 Subject: [PATCH 169/229] fix: Loan application link on creating loan --- .../loan_management/doctype/loan_application/loan_application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index bac6e638d7..e59db4c12d 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -127,6 +127,7 @@ def create_loan(source_name, target_doc=None, submit=0): target_doc.loan_account = account_details.loan_account target_doc.interest_income_account = account_details.interest_income_account target_doc.penalty_income_account = account_details.penalty_income_account + target_doc.loan_application = source_name doclist = get_mapped_doc("Loan Application", source_name, { From d82b76fe0ace6076f1087c9c0ba7552b7611cd1b Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 18 Nov 2020 11:56:49 +0530 Subject: [PATCH 170/229] fix: pos item search includes non stock items (#23914) * fix: pos item search includes non stock items * chore: add validation for non stock items --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 9 +++++++++ erpnext/selling/page/point_of_sale/point_of_sale.py | 1 + 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index a7e20a0c32..d486ff6028 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -39,6 +39,7 @@ class POSInvoice(SalesInvoice): self.validate_serialised_or_batched_item() self.validate_stock_availablility() self.validate_return_items_qty() + self.validate_non_stock_items() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() @@ -174,6 +175,14 @@ class POSInvoice(SalesInvoice): _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}") .format(d.idx, bold_serial_no, bold_return_against) ) + + def validate_non_stock_items(self): + for d in self.get("items"): + is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") + if not is_stock_item: + frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format( + d.idx, frappe.bold(d.item_code) + ), title=_("Invalid Item")) def validate_mode_of_payment(self): if len(self.payments) == 0: diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index a690050f79..062cba19e6 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -62,6 +62,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va `tabItem` item {bin_join_selection} WHERE item.disabled = 0 + AND item.is_stock_item = 1 AND item.has_variants = 0 AND item.is_sales_item = 1 AND item.is_fixed_asset = 0 From 8c9b60edfec53a8a58ace5e5a3284efbdae7b724 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 18 Nov 2020 12:51:13 +0530 Subject: [PATCH 171/229] fix: reversing previous commits and adding condition in regional controller --- erpnext/public/js/controllers/buying.js | 1 - erpnext/public/js/utils.js | 13 ------------- erpnext/regional/india/taxes.js | 1 + erpnext/regional/india/utils.py | 23 +++++++++-------------- erpnext/selling/sales_common.js | 1 - 5 files changed, 10 insertions(+), 29 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index cd5cc9282b..cb76c87b62 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -135,7 +135,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ supplier_address: function() { erpnext.utils.get_address_display(this.frm); erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address"); - erpnext.utils.set_place_of_supply(this.frm) }, buying_price_list: function() { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 1555896eac..ea2093eee1 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -116,19 +116,6 @@ $.extend(erpnext.utils, { } }, - set_place_of_supply: function(frm){ - frappe.call({ - method: "erpnext.regional.india.utils.get_place_of_supply", - args: { - "party_details": frm.doc, - "doctype": frm.doc.doctype - }, - callback: function(r){ - frm.set_value("place_of_supply", r.message) - } - }) - }, - add_indicator_for_multicompany: function(frm, info) { frm.dashboard.stats_area.removeClass('hidden'); frm.dashboard.stats_area_row.addClass('flex'); diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 3b6a28f52c..ecfa9b7cdf 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -37,6 +37,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { callback: function(r) { if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + frm.set_value('place_of_supply', r.message.place_of_supply); } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) { frm.set_value('taxes_and_charges', ''); frm.set_value('taxes', []); diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 7ad1c07f93..54083dea84 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -135,12 +135,8 @@ def test_method(): '''test function''' return 'overridden' -@frappe.whitelist() def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): 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", "Sales Order"): address_name = party_details.customer_address or party_details.shipping_address_name @@ -164,7 +160,7 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N if is_internal_transfer(party_details, doctype): party_details.taxes_and_charges = '' party_details.taxes = '' - return + return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" @@ -172,26 +168,26 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N 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: + if party_details.get('taxes_and_charges'): return party_details if not party_details.company_gstin: - return + return party_details 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: + if party_details.get('taxes_and_charges'): return party_details if not party_details.supplier_gstin: - return + return party_details - if not party_details.place_of_supply: return + if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return + if not party_details.company_gstin: return party_details 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", @@ -201,12 +197,11 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) if not default_tax: - return + return party_details party_details["taxes_and_charges"] = default_tax party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) - if return_taxes: - return party_details + return party_details def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 77bdf2912f..002cfe41e1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -117,7 +117,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ customer_address: function() { erpnext.utils.get_address_display(this.frm, "customer_address"); erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name"); - erpnext.utils.set_place_of_supply(this.frm) }, shipping_address_name: function() { From 410db04b48291e310b981b541279d0930dcf4eed Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 18 Nov 2020 15:57:16 +0530 Subject: [PATCH 172/229] fix: linter issue for translation syntax --- erpnext/regional/india/utils.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 227af9cdeb..e189da7b11 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -86,7 +86,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: frappe.throw(_("""Invalid {0}! The check digit validation has failed. - Please ensure you've typed the {0} correctly.""".format(label))) + Please ensure you've typed the {0} correctly.""").format(label)) def get_itemised_tax_breakup_header(item_doctype, tax_accounts): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): @@ -160,7 +160,7 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N if is_internal_transfer(party_details, doctype): party_details.taxes_and_charges = '' party_details.taxes = '' - return party_details + return if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" @@ -168,26 +168,26 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N 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'): + if party_details.get('taxes_and_charges') and return_taxes: return party_details if not party_details.company_gstin: - return party_details + return 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'): + if party_details.get('taxes_and_charges') and return_taxes: return party_details if not party_details.supplier_gstin: - return party_details + return - if not party_details.place_of_supply: return party_details + if not party_details.place_of_supply: return - if not party_details.company_gstin: return party_details + 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", @@ -197,11 +197,12 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) if not default_tax: - return party_details + return party_details["taxes_and_charges"] = default_tax party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) - return party_details + if return_taxes: + return party_details def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): @@ -235,7 +236,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): 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, - {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') + {'disabled': 0, 'tax_category': tax_category.name}, 'name') return default_tax def get_tax_template_for_sez(party_details, master_doctype, company, party_type): From 188657d05a9f0199de898094bbe033e51d52b930 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 18 Nov 2020 17:15:54 +0530 Subject: [PATCH 173/229] fix: email digest user not found --- erpnext/setup/doctype/email_digest/email_digest.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index b30bd7814b..cbb4c7c5de 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -48,12 +48,8 @@ class EmailDigest(Document): recipients = list(filter(lambda r: r in valid_users, self.recipient_list.split("\n"))) - original_user = frappe.session.user - if recipients: for user_id in recipients: - frappe.set_user(user_id) - frappe.set_user_lang(user_id) msg_for_this_recipient = self.get_msg_html() if msg_for_this_recipient: frappe.sendmail( @@ -64,9 +60,6 @@ class EmailDigest(Document): reference_name = self.name, unsubscribe_message = _("Unsubscribe from this Email Digest")) - frappe.set_user(original_user) - frappe.set_user_lang(original_user) - def get_msg_html(self): """Build email digest content""" frappe.flags.ignore_account_permission = True From bbe4933a523b737d4537827685de508cae8dac64 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 18 Nov 2020 20:58:59 +0530 Subject: [PATCH 174/229] fix(regional): set proper state code in ewaybill JSON when gst_category is SEZ --- erpnext/regional/india/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 6164e066cd..5458001e9d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -516,6 +516,9 @@ def get_address_details(data, doc, company_address, billing_address): data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address + + if doc.gst_category == 'SEZ': + data.toStateCode = 99 return data From 6dafe1eabfaad5619257ca7062ea568d44fff2af Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 19 Nov 2020 08:12:58 +0530 Subject: [PATCH 175/229] feat: Formula based Quality Inspection (#23916) * feat: Formula based Quality Inspection * chore: Added Test for Formula Based QI reading --- .../item_quality_inspection_parameter.json | 131 +++++++----------- .../quality_inspection/quality_inspection.py | 31 ++++- .../test_quality_inspection.py | 66 +++++++-- .../quality_inspection_reading.json | 38 ++++- .../quality_inspection_template.py | 6 +- 5 files changed, 176 insertions(+), 96 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index f1e1fd3679..888bc2de47 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -1,88 +1,57 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:28:01", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:28:01", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "specification", + "value", + "column_break_3", + "acceptance_formula" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "specification", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Parameter", - "length": 0, - "no_copy": 0, - "oldfieldname": "specification", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "specification", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Parameter", + "oldfieldname": "specification", + "oldfieldtype": "Data", + "print_width": "200px", + "reqd": 1, "width": "200px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "value", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Acceptance Criteria", - "length": 0, - "no_copy": 0, - "oldfieldname": "value", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Acceptance Criteria", + "oldfieldname": "value", + "oldfieldtype": "Data" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "description": "Simple Python formula based on numeric Readings.
Example 1: reading_1 > 0.2 and reading_1 < 0.5
\nExample 2: (reading_1 + reading_2) / 2 < 10", + "fieldname": "acceptance_formula", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Acceptance Criteria Formula" } - ], - "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": "2016-07-11 03:28:01.074316", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Quality Inspection Parameter", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2020-11-16 16:33:42.421842", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Quality Inspection Parameter", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index c3bb514184..399a63a186 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -4,15 +4,20 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe import _ +from frappe.utils import flt from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ import get_template_details -from frappe.model.mapper import get_mapped_doc class QualityInspection(Document): def validate(self): if not self.readings and self.item_code: self.get_item_specification_details() + if self.readings: + self.set_status_based_on_acceptance_formula() + def get_item_specification_details(self): if not self.quality_inspection_template: self.quality_inspection_template = frappe.db.get_value('Item', @@ -26,6 +31,7 @@ class QualityInspection(Document): child = self.append('readings', {}) child.specification = d.specification child.value = d.value + child.acceptance_formula = d.acceptance_formula child.status = "Accepted" def get_quality_inspection_template(self): @@ -58,6 +64,29 @@ class QualityInspection(Document): .format(parent_doc=self.reference_type, child_doc=doctype), (quality_inspection, self.modified, self.reference_name, self.item_code)) + def set_status_based_on_acceptance_formula(self): + for reading in self.readings: + if not reading.acceptance_formula: continue + + condition = reading.acceptance_formula + data = {} + for i in range(1, 11): + field = "reading_" + str(i) + data[field] = flt(reading.get(field)) or 0 + + try: + result = frappe.safe_eval(condition, None, data) + reading.status = "Accepted" if result else "Rejected" + except SyntaxError: + frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), + title=_("Invalid Formula")) + except NameError as e: + field = frappe.bold(e.args[0].split()[1]) + frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") + .format(reading.idx, field), + title=_("Invalid Formula")) + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index bb535c1f6a..2c40009426 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -7,6 +7,7 @@ import unittest from frappe.utils import nowdate from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError # test_records = frappe.get_test_records('Quality Inspection') @@ -17,10 +18,12 @@ class TestQualityInspection(unittest.TestCase): frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1) def test_qa_for_delivery(self): + make_stock_entry(item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + self.assertRaises(QualityInspectionRequiredError, dn.submit) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected", submit=True) + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected") dn.reload() self.assertRaises(QualityInspectionRejectedError, dn.submit) @@ -28,12 +31,51 @@ class TestQualityInspection(unittest.TestCase): dn.reload() dn.submit() + qa.cancel() + dn.reload() + dn.cancel() + def test_qa_not_submit(self): dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) - qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, submit = False) + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True) dn.items[0].quality_inspection = qa.name self.assertRaises(QualityInspectionNotSubmittedError, dn.submit) + qa.delete() + dn.delete() + + def test_formula_based_qi_readings(self): + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + readings = [{ + "specification": "Iron Content", + "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", + "reading_1": 0.4 + }, + { + "specification": "Calcium Content", + "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", + "reading_1": 0.7 + }, + { + "specification": "Mg Content", + "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9", + "reading_1": 0.5, + "reading_2": 0.7, + "reading_3": "random text" # check if random string input causes issues + }] + + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, + readings=readings, do_not_save=True) + qa.save() + + # status must be auto set as per formula + self.assertEqual(qa.readings[0].status, "Accepted") + self.assertEqual(qa.readings[1].status, "Rejected") + self.assertEqual(qa.readings[2].status, "Accepted") + + qa.delete() + dn.delete() + def create_quality_inspection(**args): args = frappe._dict(args) qa = frappe.new_doc("Quality Inspection") @@ -44,12 +86,18 @@ def create_quality_inspection(**args): qa.item_code = args.item_code or "_Test Item with QA" qa.sample_size = 1 qa.inspected_by = frappe.session.user - qa.append("readings", { - "specification": "Size", - "status": args.status - }) - qa.save() - if args.submit: - qa.submit() + + readings = args.readings or {"specification": "Size", "status": args.status} + + if isinstance(readings, list): + for entry in readings: + qa.append("readings", entry) + else: + qa.append("readings", readings) + + if not args.do_not_save: + qa.save() + if not args.do_not_submit: + qa.submit() return qa diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index f9f8a71c02..c1976dd1fb 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -1,22 +1,29 @@ { + "actions": [], "autoname": "hash", "creation": "2013-02-22 01:27:43", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "specification", "value", + "status", + "column_break_4", + "acceptance_formula", + "section_break_3", "reading_1", "reading_2", "reading_3", + "column_break_10", "reading_4", "reading_5", "reading_6", + "column_break_14", "reading_7", "reading_8", "reading_9", - "reading_10", - "status" + "reading_10" ], "fields": [ { @@ -124,15 +131,40 @@ "oldfieldname": "status", "oldfieldtype": "Select", "options": "Accepted\nRejected" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "description": "Simple Python formula based on numeric Readings.
Example 1: reading_1 > 0.2 and reading_1 < 0.5
\nExample 2: (reading_1 + reading_2) / 2 < 10", + "fieldname": "acceptance_formula", + "fieldtype": "Code", + "label": "Acceptance Criteria Formula" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, - "modified": "2019-07-11 18:48:12.667404", + "links": [], + "modified": "2020-11-16 16:34:29.947856", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", "owner": "Administrator", "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index 0d9a90312b..e2848469b8 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -12,5 +12,7 @@ class QualityInspectionTemplate(Document): def get_template_details(template): if not template: return [] - return frappe.get_all('Item Quality Inspection Parameter', fields=["specification", "value"], - filters={'parenttype': 'Quality Inspection Template', 'parent': template}, order_by="idx") \ No newline at end of file + return frappe.get_all('Item Quality Inspection Parameter', + fields=["specification", "value", "acceptance_formula"], + filters={'parenttype': 'Quality Inspection Template', 'parent': template}, + order_by="idx") \ No newline at end of file From 14514f776b7134282c98d40392d89a714ddee68e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 08:17:16 +0530 Subject: [PATCH 176/229] fix: Enable Allow Auto Repeat for standard doctypes having auto_repeat field (#23776) Co-authored-by: Shivam Mishra --- erpnext/accounts/doctype/journal_entry/journal_entry.json | 3 ++- erpnext/accounts/doctype/payment_entry/payment_entry.json | 3 ++- erpnext/accounts/doctype/pos_invoice/pos_invoice.json | 3 ++- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 6 ++++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 3 ++- erpnext/buying/doctype/purchase_order/purchase_order.json | 3 ++- .../doctype/supplier_quotation/supplier_quotation.json | 3 ++- erpnext/selling/doctype/quotation/quotation.json | 3 ++- erpnext/selling/doctype/sales_order/sales_order.json | 3 ++- erpnext/stock/doctype/delivery_note/delivery_note.json | 1 + .../stock/doctype/purchase_receipt/purchase_receipt.json | 3 ++- 11 files changed, 23 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 4573c50134..b7bbb74ce9 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-03-25 10:53:52", @@ -503,7 +504,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2020-06-02 18:15:46.955697", + "modified": "2020-10-30 13:56:01.121995", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 72149a665d..2e1f201e25 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2016-06-01 14:38:51.012597", @@ -587,7 +588,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-02 13:39:43.383705", + "modified": "2020-10-30 13:56:20.007336", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 1cff3c661d..5bc57b4a84 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2020-01-24 15:29:29.933693", @@ -1580,7 +1581,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2020-09-28 16:51:24.641755", + "modified": "2020-10-30 13:56:51.056083", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 8925b87b52..2df77a84c7 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -1334,7 +1335,8 @@ "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, - "modified": "2020-09-21 12:22:09.164068", + "links": [], + "modified": "2020-10-30 13:57:18.266978", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1396,4 +1398,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ae40153890..17fbe2def9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", @@ -1955,7 +1956,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-10-09 15:59:57.544736", + "modified": "2020-10-30 13:57:45.086303", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 4b865a98e9..71231f68c8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -1105,7 +1106,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-30 11:39:37.388249", + "modified": "2020-10-30 13:58:14.697921", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 9a092ca5c3..b39c989073 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:45", @@ -807,7 +808,7 @@ "idx": 29, "is_submittable": 1, "links": [], - "modified": "2020-10-01 20:56:17.932007", + "modified": "2020-10-30 13:58:33.043971", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 5b85187ccb..3eba62bc19 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:08", @@ -932,7 +933,7 @@ "is_submittable": 1, "links": [], "max_attachments": 1, - "modified": "2020-07-26 17:46:19.951223", + "modified": "2020-10-30 13:58:59.212060", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 77c1787c26..3d64ac3780 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-06-18 12:39:59", @@ -1460,7 +1461,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-07 14:30:01.782617", + "modified": "2020-10-30 13:59:18.628077", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 3c5129b1ab..7393c8a70e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:09", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index ce54fc883f..13c8ceb759 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -1109,7 +1110,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-08-03 23:20:26.381024", + "modified": "2020-10-30 14:00:08.347534", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From cd05b34691d7ef50d06791820afddb184259e1b3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 19 Nov 2020 11:37:08 +0530 Subject: [PATCH 177/229] fix: company filter added again --- erpnext/regional/india/utils.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e189da7b11..c6620aa92b 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -160,7 +160,7 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N if is_internal_transfer(party_details, doctype): party_details.taxes_and_charges = '' party_details.taxes = '' - return + return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" @@ -168,26 +168,26 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N 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: + if party_details.get('taxes_and_charges'): return party_details if not party_details.company_gstin: - return + return party_details 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: + if party_details.get('taxes_and_charges'): return party_details if not party_details.supplier_gstin: - return + return party_details - if not party_details.place_of_supply: return + if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return + if not party_details.company_gstin: return party_details 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", @@ -197,12 +197,11 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) if not default_tax: - return + return party_details party_details["taxes_and_charges"] = default_tax party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) - if return_taxes: - return party_details + return party_details def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): @@ -236,7 +235,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): 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') + {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') return default_tax def get_tax_template_for_sez(party_details, master_doctype, company, party_type): From c1187bc1d598d6d771f2e0379b654a63adddb84b Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 19 Nov 2020 13:10:24 +0530 Subject: [PATCH 178/229] fix: duplicate items validation for POS Invoice when allow multiple items is disabled (#23896) * fix: duplicate items validation for POS when allow multiple items in disabled * fix: variable name change for duplicate item validation Co-authored-by: pateljannat --- erpnext/controllers/selling_controller.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7504746e07..515239a982 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -416,26 +416,26 @@ class SellingController(StockController): return for d in self.get('items'): - if self.doctype == "Sales Invoice": - e = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] - f = [d.item_code, d.description, d.sales_order or d.delivery_note] + if self.doctype in ["POS Invoice","Sales Invoice"]: + stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] + non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note] elif self.doctype == "Delivery Note": - e = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or ''] - f = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice] + stock_items = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or ''] + non_stock_items = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice] elif self.doctype in ["Sales Order", "Quotation"]: - e = [d.item_code, d.description, d.warehouse, ''] - f = [d.item_code, d.description] + stock_items = [d.item_code, d.description, d.warehouse, ''] + non_stock_items = [d.item_code, d.description] if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: - if e in check_list: + if stock_items in check_list: frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) else: - check_list.append(e) + check_list.append(stock_items) else: - if f in chk_dupl_itm: + if non_stock_items in chk_dupl_itm: frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) else: - chk_dupl_itm.append(f) + chk_dupl_itm.append(non_stock_items) def validate_target_warehouse(self): items = self.get("items") + (self.get("packed_items") or []) From f0b1670abc331bf7aad8eb7d746482648b710e65 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 19 Nov 2020 18:40:13 +0530 Subject: [PATCH 179/229] fix: tds test case --- .../test_tax_withholding_category.py | 12 ++++++++---- .../buying/doctype/supplier/test_supplier.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index a0b0cbb995..ef77674372 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -7,6 +7,7 @@ import frappe import unittest from frappe.utils import today from erpnext.accounts.utils import get_fiscal_year +from erpnext.buying.doctype.supplier.test_supplier import create_supplier test_dependencies = ["Supplier Group"] @@ -103,17 +104,20 @@ class TestTaxWithholdingCategory(unittest.TestCase): def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self): invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS") - pi = create_purchase_invoice(supplier="Test TDS Supplier2") + doc = create_supplier(supplier_name = "Test TDS Supplier ABC", + tax_withholding_category="Single Threshold TDS") + supplier = doc.name + + pi = create_purchase_invoice(supplier=supplier) pi.submit() invoices.append(pi) # TDS not applied - pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True) + pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True) pi.submit() invoices.append(pi) - pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi = create_purchase_invoice(supplier=supplier) pi.submit() invoices.append(pi) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index a377ec90f8..f9c8d35518 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -120,3 +120,20 @@ class TestSupplier(unittest.TestCase): # Rollback address.delete() + +def create_supplier(**args): + args = frappe._dict(args) + + try: + doc = frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": args.supplier_name, + "supplier_group": args.supplier_group or "Services", + "supplier_type": args.supplier_type or "Company", + "tax_withholding_category": args.tax_withholding_category + }).insert() + + return doc + + except frappe.DuplicateEntryError: + return frappe.get_doc("Supplier", args.supplier_name) \ No newline at end of file From 1d5d863e9a7e71c540e9ba032ef042e218667a56 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 19 Nov 2020 20:11:45 +0530 Subject: [PATCH 180/229] fix: removing return_taxes condition --- erpnext/regional/india/taxes.js | 3 +-- erpnext/regional/india/utils.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index ecfa9b7cdf..3c156479c5 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -31,8 +31,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { args: { party_details: JSON.stringify(party_details), doctype: frm.doc.doctype, - company: frm.doc.company, - return_taxes: 1 + company: frm.doc.company }, callback: function(r) { if(r.message) { diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index c6620aa92b..8d89335717 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -150,7 +150,7 @@ def get_place_of_supply(party_details, doctype): return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) @frappe.whitelist() -def get_regional_address_details(party_details, doctype, company, return_taxes=None): +def get_regional_address_details(party_details, doctype, company): if isinstance(party_details, string_types): party_details = json.loads(party_details) party_details = frappe._dict(party_details) From ceab692f7313acfb11704dd50a72738a9c3be9c0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Nov 2020 17:57:35 +0530 Subject: [PATCH 181/229] fix: incorrect delink serial no and batch --- erpnext/controllers/stock_controller.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index f743d707f7..2d2fff8fd5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -229,9 +229,9 @@ class StockController(AccountsController): def check_expense_account(self, item): if not item.get("expense_account"): - frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \ - Account in the Items table").format(item.idx, frappe.bold(item.item_code)), - title=_("Expense Account Missing")) + msg = _("Please set an Expense Account in the Items table") + frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}") + .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing")) else: is_expense_account = frappe.db.get_value("Account", @@ -247,7 +247,9 @@ class StockController(AccountsController): for d in self.items: if not d.batch_no: continue - serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})] + serial_nos = [sr.name for sr in frappe.get_all("Serial No", + {'batch_no': d.batch_no, 'status': 'Inactive'})] + if serial_nos: frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) From 34d07b630669a6d18e3289df9561a50dcc3371fa Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 20 Nov 2020 19:22:35 +0530 Subject: [PATCH 182/229] fix: purchase receipt to purchase invoice bill date mapping (#23967) Co-authored-by: pateljannat --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d964669830..2cc4679c8c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -572,7 +572,8 @@ def make_purchase_invoice(source_name, target_doc=None): "doctype": "Purchase Invoice", "field_map": { "supplier_warehouse":"supplier_warehouse", - "is_return": "is_return" + "is_return": "is_return", + "bill_date": "bill_date" }, "validation": { "docstatus": ["=", 1], From 20d6143c382a677db5464a0b383962e746c4b3a5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 21 Nov 2020 12:09:11 +0530 Subject: [PATCH 183/229] fix: Validation for journal entry with 0 debit and credit values --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d8394785c6..0b3205523f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -34,6 +34,7 @@ class JournalEntry(AccountsController): self.validate_entries_for_advance() self.validate_multi_currency() self.set_amounts_in_company_currency() + self.validate_debit_credit_amount() self.validate_total_debit_and_credit() self.validate_against_jv() self.validate_reference_doc() @@ -369,6 +370,11 @@ class JournalEntry(AccountsController): if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) + def validate_debit_credit_amount(self): + for d in self.get('accounts'): + if not flt(d.debit) and not flt(d.credit): + frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) + def validate_total_debit_and_credit(self): self.set_total_debit_credit() if self.difference: From 610d9ca64937366da56ad2ef7610def4c5d36b57 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 23 Nov 2020 13:47:49 +0530 Subject: [PATCH 184/229] fix: bom stock report color issue --- .../report/bom_stock_report/bom_stock_report.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 2ac6fa073b..45331c6af8 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -25,11 +25,11 @@ frappe.query_reports["BOM Stock Report"] = { ], "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.id == "Item"){ + if (column.id == "item"){ if (data["Enough Parts to Build"] > 0){ - value = `${data['Item']}` + value = `${data['item']}`; } else { - value = `${data['Item']}` + value = `${data['item']}`; } } return value From 34f381df172f7c40db4f51317a22d8f29868b0d3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 23 Nov 2020 15:31:08 +0530 Subject: [PATCH 185/229] fix: enabling track changes for stock settings --- erpnext/stock/doctype/stock_settings/stock_settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 067659f64a..a1666579d1 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -217,7 +217,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 10:33:29.147682", + "modified": "2020-11-23 15:26:54.225608", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -235,5 +235,6 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file From f9a44000d9b06517dc2fd10916eac3f8d0c02b8e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 23 Nov 2020 15:40:06 +0530 Subject: [PATCH 186/229] Update bom_stock_report.js --- .../manufacturing/report/bom_stock_report/bom_stock_report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 45331c6af8..84f5c346ca 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -25,8 +25,8 @@ frappe.query_reports["BOM Stock Report"] = { ], "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (column.id == "item"){ - if (data["Enough Parts to Build"] > 0){ + if (column.id == "item") { + if (data["Enough Parts to Build"] > 0) { value = `${data['item']}`; } else { value = `${data['item']}`; From 6aa6ec1832d9c4caf00e4f113845b47f80dd92bb Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 24 Nov 2020 08:01:19 +0530 Subject: [PATCH 187/229] fix: clear error message when approval not available (#23971) --- .../department_approver/department_approver.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 9b2de0e1cb..d337959d53 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -20,7 +20,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) + employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) employee_department = filters.get("department") or employee.department if employee_department: @@ -59,11 +59,9 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) if len(approvers) == 0: - frappe.throw(_("Please set {0} for the Employee or for Department: {1}"). - format( - field_name, frappe.bold(employee_department), - frappe.bold(employee.name) - ), - title=_(field_name + " Missing")) + error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name)) + if department_list: + error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department)) + frappe.throw(error_msg, title=_(field_name + " Missing")) return set(tuple(approver) for approver in approvers) From d07447aa5fbeed93e72e882230e6b571a47b6611 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 24 Nov 2020 08:09:17 +0530 Subject: [PATCH 188/229] fix: Validation for duplicate Tax Category (#23978) * fix: Validation for duplicate Tax Category * Update utils.py Co-authored-by: Nabin Hait --- erpnext/hooks.py | 3 +++ erpnext/regional/india/utils.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b4c57d7c91..741176f33f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -237,6 +237,9 @@ doc_events = { "Website Settings": { "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" }, + "Tax Category": { + "validate": "erpnext.regional.india.utils.validate_tax_category" + }, "Sales Invoice": { "on_submit": [ "erpnext.regional.create_transaction_log", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fc38ed0972..62487ba2aa 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -51,6 +51,13 @@ 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 validate_tax_category(doc, method): + if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}): + if doc.is_inter_state: + frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) + else: + frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state)) + def update_gst_category(doc, method): for link in doc.links: if link.link_doctype in ['Customer', 'Supplier']: @@ -85,8 +92,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): total += digit factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: - frappe.throw(_("""Invalid {0}! The check digit validation has failed. - Please ensure you've typed the {0} correctly.""").format(label)) + frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) def get_itemised_tax_breakup_header(item_doctype, tax_accounts): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): @@ -515,7 +521,7 @@ def get_address_details(data, doc, company_address, billing_address): data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address - + if doc.gst_category == 'SEZ': data.toStateCode = 99 @@ -754,4 +760,4 @@ def make_regional_gl_entries(gl_entries, doc): }, account_currency, item=tax) ) - return gl_entries \ No newline at end of file + return gl_entries From e09037ed2c1e5cf634fbd993e1d135cae84b0673 Mon Sep 17 00:00:00 2001 From: Krushnal Patel Date: Tue, 24 Nov 2020 12:53:30 +0530 Subject: [PATCH 189/229] docs: README build status badge (#23933) * fixed build status badge * changed build branch from `master` to `develop` * updated build status badge url * Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f6a52142b..15782a2e0c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

ERP made simple

-[![Build Status](https://travis-ci.com/frappe/erpnext.svg)](https://travis-ci.com/frappe/erpnext) +[![Build Status](https://api.travis-ci.com/frappe/erpnext.svg?branch=develop)](https://travis-ci.com/frappe/erpnext) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop) From 927106f5528bfebe8d43ae24ac627ae9965720d5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 24 Nov 2020 15:02:52 +0530 Subject: [PATCH 190/229] fix: maintain stock can't be changed it there is product bundle --- erpnext/stock/doctype/item/item.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3b62c38b86..be845d9d9d 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -977,15 +977,20 @@ class Item(WebsiteGenerator): # For "Is Stock Item", following doctypes is important # because reserved_qty, ordered_qty and requested_qty updated from these doctypes if field == "is_stock_item": - linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item"] + linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"] for doctype in linked_doctypes: + filters={"item_code": self.name, "docstatus": 1} + + if doctype == "Product Bundle": + filters={"new_item_code": self.name} + if doctype in ("Purchase Invoice Item", "Sales Invoice Item",): # If Invoice has Stock impact, only then consider it. if self.stock_ledger_created(): return True - elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}): + elif frappe.db.get_value(doctype, filters): return True def validate_auto_reorder_enabled_in_stock_settings(self): From 43a830f3f593f463f695c7419e1b7961ff96d79d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 24 Nov 2020 15:10:36 +0530 Subject: [PATCH 191/229] fix: Old shopify order syncing date --- .../connectors/shopify_connection.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index 8aa7453bd6..efbaa71924 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -149,26 +149,28 @@ def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=Fal si.shopify_order_number = shopify_order.get("name") si.set_posting_time = 1 si.posting_date = posting_date + si.due_date = posting_date si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-" si.flags.ignore_mandatory = True set_cost_center(si.items, shopify_settings.cost_center) si.insert(ignore_mandatory=True) si.submit() - make_payament_entry_against_sales_invoice(si, shopify_settings) + make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date) frappe.db.commit() def set_cost_center(items, cost_center): for item in items: item.cost_center = cost_center -def make_payament_entry_against_sales_invoice(doc, shopify_settings): +def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - payemnt_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account) - payemnt_entry.flags.ignore_mandatory = True - payemnt_entry.reference_no = doc.name - payemnt_entry.reference_date = nowdate() - payemnt_entry.insert(ignore_permissions=True) - payemnt_entry.submit() + payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account) + payment_entry.flags.ignore_mandatory = True + payment_entry.reference_no = doc.name + payment_entry.posting_date = posting_date or nowdate() + payment_entry.reference_date = posting_date or nowdate() + payment_entry.insert(ignore_permissions=True) + payment_entry.submit() def create_delivery_note(shopify_order, shopify_settings, so): if not cint(shopify_settings.sync_delivery_note): From b67ebc7636187f1e4ee508579a77cbb6a7e223d1 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 24 Nov 2020 15:37:30 +0530 Subject: [PATCH 192/229] fix: job card error handling for operations field --- .../doctype/job_card/job_card.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 4dfa78bf21..d15d81ed93 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -353,17 +353,19 @@ def get_operation_details(work_order, operation): @frappe.whitelist() def get_operations(doctype, txt, searchfield, start, page_len, filters): - if filters.get("work_order"): - args = {"parent": filters.get("work_order")} - if txt: - args["operation"] = ("like", "%{0}%".format(txt)) + if not filters.get("work_order"): + frappe.msgprint(_("Please select a Work Order first.")) + return [] + args = {"parent": filters.get("work_order")} + if txt: + args["operation"] = ("like", "%{0}%".format(txt)) - return frappe.get_all("Work Order Operation", - filters = args, - fields = ["distinct operation as operation"], - limit_start = start, - limit_page_length = page_len, - order_by="idx asc", as_list=1) + return frappe.get_all("Work Order Operation", + filters = args, + fields = ["distinct operation as operation"], + limit_start = start, + limit_page_length = page_len, + order_by="idx asc", as_list=1) @frappe.whitelist() def make_material_request(source_name, target_doc=None): From 5a33f2c394aceb03be6b00e142e6dea25695d1d3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 24 Nov 2020 16:59:36 +0530 Subject: [PATCH 193/229] fix: bom stock report color showing always red --- .../manufacturing/report/bom_stock_report/bom_stock_report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 84f5c346ca..8cd016461c 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -26,7 +26,7 @@ frappe.query_reports["BOM Stock Report"] = { "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (column.id == "item") { - if (data["Enough Parts to Build"] > 0) { + if (data["enough_parts_to_build"] > 0) { value = `${data['item']}`; } else { value = `${data['item']}`; From e4755828c4690404939ad7a5cc53a0d6ca988e18 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Tue, 24 Nov 2020 23:31:06 +0530 Subject: [PATCH 194/229] fix: template errors in pricing rule (#23999) * fix: solve microtemplating errors --- .../doctype/pricing_rule/pricing_rule.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js index c92b58b580..d79ad5f528 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js @@ -42,56 +42,56 @@ frappe.ui.form.on('Pricing Rule', {

- ${__('Notes')} + {{__('Notes')}}

  • - ${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")} + {{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
  • - ${__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")} + {{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
  • - ${__('Discount Percentage can be applied either against a Price List or for all Price List.')} + {{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
  • - ${__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')} + {{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}

- ${__('How Pricing Rule is applied?')} + {{__('How Pricing Rule is applied?')}}

  1. - ${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")} + {{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
  2. - ${__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")} + {{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
  3. - ${__('Pricing Rules are further filtered based on quantity.')} + {{__('Pricing Rules are further filtered based on quantity.')}}
  4. - ${__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')} + {{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
  5. - ${__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')} + {{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
    • - ${__('Item Code > Item Group > Brand')} + {{__('Item Code > Item Group > Brand')}}
    • - ${__('Customer > Customer Group > Territory')} + {{__('Customer > Customer Group > Territory')}}
    • - ${__('Supplier > Supplier Type')} + {{__('Supplier > Supplier Type')}}
  6. - ${__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')} + {{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
From a339752ba4419a5a00530286499282ebb0cb4216 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 25 Nov 2020 08:54:51 +0530 Subject: [PATCH 195/229] fix: Loan disbursement amount validation (#24000) --- erpnext/loan_management/doctype/loan/loan.json | 3 ++- .../doctype/loan_disbursement/loan_disbursement.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index e8ecf015c3..d468f52bc0 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -332,6 +332,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.is_secured_loan", "fetch_from": "loan_application.maximum_loan_amount", "fieldname": "maximum_loan_amount", "fieldtype": "Currency", @@ -352,7 +353,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-05 10:04:00.762975", + "modified": "2020-11-24 12:27:23.208240", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 233862bcfe..f341e81065 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -171,10 +171,10 @@ def get_total_pledged_security_value(loan): return security_value @frappe.whitelist() -def get_disbursal_amount(loan): - loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", - "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], - filters= { "name": loan })[0] +def get_disbursal_amount(loan, on_current_security_price=0): + loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment", + "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan", + "maximum_loan_amount"], as_dict=1) if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, 'status': 'Pending'}): @@ -188,9 +188,12 @@ def get_disbursal_amount(loan): - flt(loan_details.total_principal_paid) security_value = 0.0 - if loan_details.is_secured_loan: + if loan_details.is_secured_loan and on_current_security_price: security_value = get_total_pledged_security_value(loan) + if loan_details.is_secured_loan and not on_current_security_price: + security_value = flt(loan_details.maximum_loan_amount) + if not security_value and not loan_details.is_secured_loan: security_value = flt(loan_details.loan_amount) From c66bd45ba46b9e7c6ebd54eda42db4e6fb57761a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 25 Nov 2020 09:09:40 +0530 Subject: [PATCH 196/229] feat: Inpatient Medication Orders Script Report (#23984) * feat: Inpatient Medication Orders Script Report * feat: add chart for Inpatient Medication Order Report * feat: add report to Desk Page * feat: added filters for dates and healthcare service unit * test: Inpatient Medication Orders report --- .../desk_page/healthcare/healthcare.json | 4 +- .../inpatient_medication_entry.py | 4 +- .../inpatient_medication_orders/__init__.py | 0 .../inpatient_medication_orders.js | 57 +++++ .../inpatient_medication_orders.json | 36 ++++ .../inpatient_medication_orders.py | 198 ++++++++++++++++++ .../test_inpatient_medication_orders.py | 128 +++++++++++ 7 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/__init__.py create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 6546b08db9..81d60481ce 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t}\n]" + "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Inpatient Medication Orders\",\n\t\t\"doctype\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Orders\"\n\t}\n]" } ], "category": "Domains", @@ -64,7 +64,7 @@ "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-06-25 23:50:56.951698", + "modified": "2020-11-23 23:00:48.764377", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index 23e75196ee..5dac23abd9 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -274,4 +274,6 @@ def get_filters(entry): def get_current_healthcare_service_unit(inpatient_record): ip_record = frappe.get_doc('Inpatient Record', inpatient_record) - return ip_record.inpatient_occupancies[-1].service_unit \ No newline at end of file + if ip_record.inpatient_occupancies: + return ip_record.inpatient_occupancies[-1].service_unit + return \ No newline at end of file diff --git a/erpnext/healthcare/report/inpatient_medication_orders/__init__.py b/erpnext/healthcare/report/inpatient_medication_orders/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js new file mode 100644 index 0000000000..a10f83760f --- /dev/null +++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js @@ -0,0 +1,57 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Inpatient Medication Orders"] = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.now_date(), + reqd: 1 + }, + { + fieldname: "patient", + label: __("Patient"), + fieldtype: "Link", + options: "Patient" + }, + { + fieldname: "service_unit", + label: __("Healthcare Service Unit"), + fieldtype: "Link", + options: "Healthcare Service Unit", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'is_group': 0 + } + } + } + }, + { + fieldname: "show_completed_orders", + label: __("Show Completed Orders"), + fieldtype: "Check", + default: 1 + } + ] +}; diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json new file mode 100644 index 0000000000..9217fa1891 --- /dev/null +++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json @@ -0,0 +1,36 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-11-23 17:25:58.802949", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2020-11-23 19:40:20.227591", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Inpatient Medication Orders", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Inpatient Medication Order", + "report_name": "Inpatient Medication Orders", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Healthcare Administrator" + }, + { + "role": "Nursing User" + }, + { + "role": "Physician" + } + ] +} \ No newline at end of file diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py new file mode 100644 index 0000000000..b9077301ba --- /dev/null +++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py @@ -0,0 +1,198 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + chart = get_chart_data(data) + + return columns, data, None, chart + +def get_columns(): + return [ + { + "fieldname": "patient", + "fieldtype": "Link", + "label": "Patient", + "options": "Patient", + "width": 200 + }, + { + "fieldname": "healthcare_service_unit", + "fieldtype": "Link", + "label": "Healthcare Service Unit", + "options": "Healthcare Service Unit", + "width": 150 + }, + { + "fieldname": "drug", + "fieldtype": "Link", + "label": "Drug Code", + "options": "Item", + "width": 150 + }, + { + "fieldname": "drug_name", + "fieldtype": "Data", + "label": "Drug Name", + "width": 150 + }, + { + "fieldname": "dosage", + "fieldtype": "Link", + "label": "Dosage", + "options": "Prescription Dosage", + "width": 80 + }, + { + "fieldname": "dosage_form", + "fieldtype": "Link", + "label": "Dosage Form", + "options": "Dosage Form", + "width": 100 + }, + { + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "width": 100 + }, + { + "fieldname": "time", + "fieldtype": "Time", + "label": "Time", + "width": 100 + }, + { + "fieldname": "is_completed", + "fieldtype": "Check", + "label": "Is Order Completed", + "width": 100 + }, + { + "fieldname": "healthcare_practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner", + "options": "Healthcare Practitioner", + "width": 200 + }, + { + "fieldname": "inpatient_medication_entry", + "fieldtype": "Link", + "label": "Inpatient Medication Entry", + "options": "Inpatient Medication Entry", + "width": 200 + }, + { + "fieldname": "inpatient_record", + "fieldtype": "Link", + "label": "Inpatient Record", + "options": "Inpatient Record", + "width": 200 + } + ] + +def get_data(filters): + conditions, values = get_conditions(filters) + + data = frappe.db.sql(""" + SELECT + parent.patient, parent.inpatient_record, parent.practitioner, + child.drug, child.drug_name, child.dosage, child.dosage_form, + child.date, child.time, child.is_completed, child.name + FROM `tabInpatient Medication Order` parent + INNER JOIN `tabInpatient Medication Order Entry` child + ON child.parent = parent.name + WHERE + parent.docstatus = 1 + {conditions} + ORDER BY date, time + """.format(conditions=conditions), values, as_dict=1) + + data = get_inpatient_details(data, filters.get("service_unit")) + + return data + +def get_conditions(filters): + conditions = "" + values = dict() + + if filters.get("company"): + conditions += " AND parent.company = %(company)s" + values["company"] = filters.get("company") + + if filters.get("from_date") and filters.get("to_date"): + conditions += " AND child.date BETWEEN %(from_date)s and %(to_date)s" + values["from_date"] = filters.get("from_date") + values["to_date"] = filters.get("to_date") + + if filters.get("patient"): + conditions += " AND parent.patient = %(patient)s" + values["patient"] = filters.get("patient") + + if not filters.get("show_completed_orders"): + conditions += " AND child.is_completed = 0" + + return conditions, values + + +def get_inpatient_details(data, service_unit): + service_unit_filtered_data = [] + + for entry in data: + entry["healthcare_service_unit"] = get_current_healthcare_service_unit(entry.inpatient_record) + if entry.is_completed: + entry["inpatient_medication_entry"] = get_inpatient_medication_entry(entry.name) + + if service_unit and entry.healthcare_service_unit and service_unit != entry.healthcare_service_unit: + service_unit_filtered_data.append(entry) + + entry.pop("name", None) + + for entry in service_unit_filtered_data: + data.remove(entry) + + return data + +def get_inpatient_medication_entry(order_entry): + return frappe.db.get_value("Inpatient Medication Entry Detail", {"against_imoe": order_entry}, "parent") + +def get_chart_data(data): + if not data: + return None + + labels = ["Pending", "Completed"] + datasets = [] + + status_wise_data = { + "Pending": 0, + "Completed": 0 + } + + for d in data: + if d.is_completed: + status_wise_data["Completed"] += 1 + else: + status_wise_data["Pending"] += 1 + + datasets.append({ + "name": "Inpatient Medication Order Status", + "values": [status_wise_data.get("Pending"), status_wise_data.get("Completed")] + }) + + chart = { + "data": { + "labels": labels, + "datasets": datasets + }, + "type": "donut", + "height": 300 + } + + chart["fieldtype"] = "Data" + + return chart \ No newline at end of file diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py new file mode 100644 index 0000000000..0d3f45f500 --- /dev/null +++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py @@ -0,0 +1,128 @@ +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import unittest +import frappe +import datetime +from frappe.utils import getdate, now_datetime +from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy +from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge +from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme +from erpnext.healthcare.report.inpatient_medication_orders.inpatient_medication_orders import execute + +class TestInpatientMedicationOrders(unittest.TestCase): + @classmethod + def setUpClass(self): + frappe.db.sql("delete from `tabInpatient Medication Order` where company='_Test Company'") + frappe.db.sql("delete from `tabInpatient Medication Entry` where company='_Test Company'") + self.patient = create_patient() + self.ip_record = create_records(self.patient) + + def test_inpatient_medication_orders_report(self): + filters = { + 'company': '_Test Company', + 'from_date': getdate(), + 'to_date': getdate(), + 'patient': '_Test IPD Patient', + 'service_unit': 'Test Service Unit Ip Occupancy - _TC' + } + + report = execute(filters) + + expected_data = [ + { + 'patient': '_Test IPD Patient', + 'inpatient_record': self.ip_record.name, + 'practitioner': None, + 'drug': 'Dextromethorphan', + 'drug_name': 'Dextromethorphan', + 'dosage': 1.0, + 'dosage_form': 'Tablet', + 'date': getdate(), + 'time': datetime.timedelta(seconds=32400), + 'is_completed': 0, + 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + }, + { + 'patient': '_Test IPD Patient', + 'inpatient_record': self.ip_record.name, + 'practitioner': None, + 'drug': 'Dextromethorphan', + 'drug_name': 'Dextromethorphan', + 'dosage': 1.0, + 'dosage_form': 'Tablet', + 'date': getdate(), + 'time': datetime.timedelta(seconds=50400), + 'is_completed': 0, + 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + }, + { + 'patient': '_Test IPD Patient', + 'inpatient_record': self.ip_record.name, + 'practitioner': None, + 'drug': 'Dextromethorphan', + 'drug_name': 'Dextromethorphan', + 'dosage': 1.0, + 'dosage_form': 'Tablet', + 'date': getdate(), + 'time': datetime.timedelta(seconds=75600), + 'is_completed': 0, + 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC' + } + ] + + self.assertEqual(expected_data, report[1]) + + filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='') + ipme = create_ipme(filters) + ipme.submit() + + filters = { + 'company': '_Test Company', + 'from_date': getdate(), + 'to_date': getdate(), + 'patient': '_Test IPD Patient', + 'service_unit': 'Test Service Unit Ip Occupancy - _TC', + 'show_completed_orders': 0 + } + + report = execute(filters) + self.assertEqual(len(report[1]), 0) + + def tearDown(self): + if frappe.db.get_value('Patient', self.patient, 'inpatient_record'): + # cleanup - Discharge + schedule_discharge(frappe.as_json({'patient': self.patient})) + self.ip_record.reload() + mark_invoiced_inpatient_occupancy(self.ip_record) + + self.ip_record.reload() + discharge_patient(self.ip_record) + + for entry in frappe.get_all('Inpatient Medication Entry'): + doc = frappe.get_doc('Inpatient Medication Entry', entry.name) + doc.cancel() + doc.delete() + + for entry in frappe.get_all('Inpatient Medication Order'): + doc = frappe.get_doc('Inpatient Medication Order', entry.name) + doc.cancel() + doc.delete() + + +def create_records(patient): + frappe.db.sql("""delete from `tabInpatient Record`""") + + # Admit + ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 + ip_record.save() + ip_record.reload() + service_unit = get_healthcare_service_unit() + admit_patient(ip_record, service_unit, now_datetime()) + + ipmo = create_ipmo(patient) + ipmo.submit() + + return ip_record From fbcc3c1b7006069d9bb9739ef57dc3776675a3b4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 25 Nov 2020 04:41:51 +0100 Subject: [PATCH 197/229] fix: Translatable strings (#23783) * fix: start_pattern * fix: translatable strings * fix: add missing semicolon (task) * fix: add missing semicolon (setup_wizard) * fix: text should start on the same line Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * fix: move out HTML element as variable Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * fix: pull out message, translate "Undo". Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * fix: typo Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * fix: text should start on the same line Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * Revert "fix: start_pattern" This reverts commit decc62e2ab75f45db1df022fe13780c2d0d2560d. Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- .../chart_of_accounts_importer.js | 3 +-- .../bank_reconciliation.js | 6 ++---- erpnext/assets/doctype/asset/asset.js | 6 +++--- .../appointment_booking_settings.js | 2 +- erpnext/projects/doctype/task/task.js | 5 ++++- erpnext/public/js/hub/pages/Category.vue | 2 +- erpnext/public/js/hub/pages/FeaturedItems.vue | 12 +++++------- erpnext/public/js/hub/pages/Item.vue | 8 ++++---- erpnext/public/js/hub/pages/NotFound.vue | 2 +- erpnext/public/js/hub/pages/Publish.vue | 19 +++++++------------ erpnext/public/js/hub/pages/SavedItems.vue | 11 ++++++++--- erpnext/public/js/hub/pages/Search.vue | 5 ++++- erpnext/public/js/hub/pages/Seller.vue | 4 ++-- erpnext/public/js/setup_wizard.js | 5 ++++- 14 files changed, 47 insertions(+), 43 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 2235298201..f795dfa83e 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -94,8 +94,7 @@ frappe.ui.form.on('Chart of Accounts Importer', { callback: function(r) { if(r.message===false) { frm.set_value("company", ""); - frappe.throw(__(`Transactions against the company already exist! - Chart Of accounts can be imported for company with no transactions`)); + frappe.throw(__("Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions.")); } else { frm.trigger("refresh"); } diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js index 9703527875..6ae81d7402 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js @@ -156,7 +156,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { setup_transactions_dom() { const me = this; - me.parent.$main_section.append(`
`) + me.parent.$main_section.append('
'); } create_datatable() { @@ -167,9 +167,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { }) } catch(err) { - let msg = __(`Your file could not be processed by ERPNext. -
It should be a standard CSV or XLSX file. -
The headers should be in the first row.`) + let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row."); frappe.throw(msg) } diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 7ad164a8b9..b2318a2bc6 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -373,8 +373,8 @@ frappe.ui.form.on('Asset', { doctype_field = frappe.scrub(doctype) frm.set_value(doctype_field, ''); frappe.msgprint({ - title: __(`Invalid ${doctype}`), - message: __(`The selected ${doctype} doesn't contains selected Asset Item.`), + title: __('Invalid {0}', [__(doctype)]), + message: __('The selected {0} does not contain the selected Asset Item.', [__(doctype)]), indicator: 'red' }); } @@ -436,7 +436,7 @@ frappe.ui.form.on('Asset Finance Book', { depreciation_start_date: function(frm, cdt, cdn) { const book = locals[cdt][cdn]; if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { - frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`)); + frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date.")); book.depreciation_start_date = ""; frm.refresh_field("finance_books"); } diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js index 99b82148d2..dc3ae8bf41 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -4,7 +4,7 @@ function check_times(frm) { let from_time = Date.parse('01/01/2019 ' + d.from_time); let to_time = Date.parse('01/01/2019 ' + d.to_time); if (from_time > to_time) { - frappe.throw(__(`In row ${i + 1} of Appointment Booking Slots : "To Time" must be later than "From Time"`)); + frappe.throw(__('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [i + 1])); } }); } \ No newline at end of file diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js index 8c6a9cf8d7..002ddb2f40 100644 --- a/erpnext/projects/doctype/task/task.js +++ b/erpnext/projects/doctype/task/task.js @@ -49,7 +49,10 @@ frappe.ui.form.on("Task", { }, callback: function (r) { if (r.message.length > 0) { - frappe.msgprint(__(`Cannot convert it to non-group. The following child Tasks exist: ${r.message.join(", ")}.`)); + let message = __('Cannot convert Task to non-group because the following child Tasks exist: {0}.', + [r.message.join(", ")] + ); + frappe.msgprint(message); frm.reload_doc(); } } diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue index 057fe8bc61..16d06018ff 100644 --- a/erpnext/public/js/hub/pages/Category.vue +++ b/erpnext/public/js/hub/pages/Category.vue @@ -32,7 +32,7 @@ export default { item_id_fieldname: 'name', // Constants - empty_state_message: __(`No items in this category yet.`), + empty_state_message: __('No items in this category yet.'), search_value: '', diff --git a/erpnext/public/js/hub/pages/FeaturedItems.vue b/erpnext/public/js/hub/pages/FeaturedItems.vue index ab9990a323..63ae7e99bb 100644 --- a/erpnext/public/js/hub/pages/FeaturedItems.vue +++ b/erpnext/public/js/hub/pages/FeaturedItems.vue @@ -33,10 +33,8 @@ export default { // Constants page_title: __('Your Featured Items'), - empty_state_message: __(`No featured items yet. Got to your - - Published Items - and feature upto 8 items that you want to highlight to your customers.`) + empty_state_message: __('No featured items yet. Got to your {0} and feature up to eight items that you want to highlight to your customers.', + [`${__("Published Items")}`]) }; }, created() { @@ -71,9 +69,9 @@ export default { const item_name = this.items.filter(item => item.hub_item_name === hub_item_name); - alert = frappe.show_alert(__(`${item_name} removed. - Undo`), - grace_period/1000, + alert_message = __('{0} removed. {1}', [item_name, + `${__('Undo')}`]); + alert = frappe.show_alert(alert_message, grace_period / 1000, { 'undo-remove': undo_remove.bind(this) } diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue index 51ade42cba..93002a7b27 100644 --- a/erpnext/public/js/hub/pages/Item.vue +++ b/erpnext/public/js/hub/pages/Item.vue @@ -113,12 +113,12 @@ export default { let stats = __('No views yet'); if (this.item.view_count) { - const views_message = __(`${this.item.view_count} Views`); + const views_message = __('{0} Views', [this.item.view_count]); const rating_html = get_rating_html(this.item.average_rating); const rating_count = this.item.no_of_ratings > 0 - ? `${this.item.no_of_ratings} reviews` + ? __('{0} reviews', [this.item.no_of_ratings]) : __('No reviews yet'); stats = [views_message, rating_html, rating_count]; @@ -310,7 +310,7 @@ export default { return this.get_item_details(); }) .then(() => { - frappe.show_alert(__(`${this.item.item_name} Updated`)); + frappe.show_alert(__('{0} Updated', [this.item.item_name])); }); }, @@ -337,7 +337,7 @@ export default { }, unpublish_item() { - frappe.confirm(__(`Unpublish {0}?`, [this.item.item_name]), () => { + frappe.confirm(__('Unpublish {0}?', [this.item.item_name]), () => { frappe .call('erpnext.hub_node.api.unpublish_item', { item_code: this.item.item_code, diff --git a/erpnext/public/js/hub/pages/NotFound.vue b/erpnext/public/js/hub/pages/NotFound.vue index 246d31bc68..8901b97802 100644 --- a/erpnext/public/js/hub/pages/NotFound.vue +++ b/erpnext/public/js/hub/pages/NotFound.vue @@ -27,7 +27,7 @@ export default { }, // Constants - empty_state_message: __(`Sorry! I could not find what you were looking for.`) + empty_state_message: __('Sorry! We could not find what you were looking for.') }; }, } diff --git a/erpnext/public/js/hub/pages/Publish.vue b/erpnext/public/js/hub/pages/Publish.vue index 735f2b92ec..96fa0aae4e 100644 --- a/erpnext/public/js/hub/pages/Publish.vue +++ b/erpnext/public/js/hub/pages/Publish.vue @@ -75,14 +75,11 @@ export default { // TODO: multiline translations don't work page_title: __('Publish Items'), search_placeholder: __('Search Items ...'), - empty_state_message: __(`No Items selected yet. Browse and click on items below to publish.`), - valid_items_instruction: __(`Only items with an image and description can be published. Please update them if an item in your inventory does not appear.`), + empty_state_message: __('No Items selected yet. Browse and click on items below to publish.'), + valid_items_instruction: __('Only items with an image and description can be published. Please update them if an item in your inventory does not appear.'), last_sync_message: (hub.settings.last_sync_datetime) - ? __(`Last sync was - - ${comment_when(hub.settings.last_sync_datetime)}. - - See your Published Items.`) + ? __('Last sync was {0}.', [`${comment_when(hub.settings.last_sync_datetime)}`]) + + ` ${__('See your Published Items.')}` : '' }; }, @@ -147,11 +144,9 @@ export default { }, add_last_sync_message() { - this.last_sync_message = __(`Last sync was - - ${comment_when(hub.settings.last_sync_datetime)}. - - See your Published Items.`); + this.last_sync_message = __('Last sync was {0}.', + [`${comment_when(hub.settings.last_sync_datetime)}`] + ) + `${__('See your Published Items')}.`; }, clear_last_sync_message() { diff --git a/erpnext/public/js/hub/pages/SavedItems.vue b/erpnext/public/js/hub/pages/SavedItems.vue index c29675acd3..7007ddcf8e 100644 --- a/erpnext/public/js/hub/pages/SavedItems.vue +++ b/erpnext/public/js/hub/pages/SavedItems.vue @@ -29,7 +29,7 @@ export default { // Constants page_title: __('Saved Items'), - empty_state_message: __(`You haven't saved any items yet.`) + empty_state_message: __('You have not saved any items yet.') }; }, created() { @@ -64,8 +64,13 @@ export default { const item_name = this.items.filter(item => item.hub_item_name === hub_item_name); - alert = frappe.show_alert(__(`${item_name} removed. - Undo`), + alert = frappe.show_alert(` + + ${__('{0} removed.', [item_name], 'A specific Item has been removed.')} + + ${__('Undo', None, 'Undo removal of item.')} + + `, grace_period/1000, { 'undo-remove': undo_remove.bind(this) diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue index 103284289b..c10841e984 100644 --- a/erpnext/public/js/hub/pages/Search.vue +++ b/erpnext/public/js/hub/pages/Search.vue @@ -42,7 +42,10 @@ export default { computed: { page_title() { return this.items.length - ? __(`Results for "${this.search_value}" ${this.category !== 'All'? `in category ${this.category}` : ''}`) + ? __('Results for "{0}" {1}', [ + this.search_value, + this.category !== 'All' ? __('in category {0}', [this.category]) : '' + ]) : __('No Items found.'); } }, diff --git a/erpnext/public/js/hub/pages/Seller.vue b/erpnext/public/js/hub/pages/Seller.vue index e339eaa3e5..c0903c64c3 100644 --- a/erpnext/public/js/hub/pages/Seller.vue +++ b/erpnext/public/js/hub/pages/Seller.vue @@ -136,7 +136,7 @@ export default { this.init = false; this.profile = data.profile; this.items = data.items; - this.item_container_heading = data.is_featured_item? "Features Items":"Popular Items"; + this.item_container_heading = data.is_featured_item ? __('Featured Items') : __('Popular Items'); this.hub_seller = this.items[0].hub_seller; this.recent_seller_reviews = data.recent_seller_reviews; this.seller_product_view_stats = data.seller_product_view_stats; @@ -147,7 +147,7 @@ export default { this.country = __(profile.country); this.site_name = __(profile.site_name); - this.joined_when = __(`Joined ${comment_when(profile.creation)}`); + this.joined_when = __('Joined {0}', [comment_when(profile.creation)]); this.image = profile.logo; this.sections = [ diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index 5d21190e37..092f83903e 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -161,7 +161,10 @@ erpnext.setup.slides_settings = [ if(r.message){ exist = r.message; me.get_field("bank_account").set_value(""); - frappe.msgprint(__(`Account ${me.values.bank_account} already exists, enter a different name for your bank account`)); + let message = __('Account {0} already exists. Please enter a different name for your bank account.', + [me.values.bank_account] + ); + frappe.msgprint(message); } } }); From 0508e6bdfaaac6771ec65e4e84fc5798ddf4785e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 25 Nov 2020 09:16:12 +0530 Subject: [PATCH 198/229] fix: Added link of bank reconciliation and clearance in accounting desk page (#23809) --- erpnext/accounts/desk_page/accounting/accounting.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index b2a3f83e5f..a18dbffd9a 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, From 6b57cf32854bac4fc95e1c27beb67a2494aba4bb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 25 Nov 2020 09:17:16 +0530 Subject: [PATCH 199/229] feat: Quality Inspection on Job Card (#23964) * feat: Quality Inspection on Job Card * fix(Job Card): quality inspection filter query * fix: sider issues --- .../doctype/job_card/job_card.js | 10 +++ .../doctype/job_card/job_card.json | 11 ++- .../quality_inspection/quality_inspection.js | 24 ++++-- .../quality_inspection.json | 4 +- .../quality_inspection/quality_inspection.py | 85 +++++++++++++------ 5 files changed, 96 insertions(+), 38 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index b051b3243f..4e8dd41022 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -31,6 +31,16 @@ frappe.ui.form.on('Job Card', { } } + frm.set_query("quality_inspection", function() { + return { + query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", + filters: { + "item_code": frm.doc.production_item, + "reference_name": frm.doc.name + } + }; + }); + frm.trigger("toggle_operation_number"); if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 575e719043..5713f697e9 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -20,6 +20,7 @@ "production_item", "item_name", "for_quantity", + "quality_inspection", "wip_warehouse", "column_break_12", "employee", @@ -305,11 +306,19 @@ "label": "Sequence Id", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval:!doc.__islocal;", + "fieldname": "quality_inspection", + "fieldtype": "Link", + "label": "Quality Inspection", + "no_copy": 1, + "options": "Quality Inspection" } ], "is_submittable": 1, "links": [], - "modified": "2020-10-14 12:58:25.327897", + "modified": "2020-11-19 18:26:50.531664", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 22f29e05b4..376848afaa 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -31,17 +31,27 @@ frappe.ui.form.on("Quality Inspection", { // item code based on GRN/DN cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { - const doctype = (doc.reference_type == "Stock Entry") ? - "Stock Entry Detail" : doc.reference_type + " Item"; + let doctype = doc.reference_type; + + if (doc.reference_type !== "Job Card") { + doctype = (doc.reference_type == "Stock Entry") ? + "Stock Entry Detail" : doc.reference_type + " Item"; + } if (doc.reference_type && doc.reference_name) { + let filters = { + "from": doctype, + "inspection_type": doc.inspection_type + }; + + if (doc.reference_type == doctype) + filters["reference_name"] = doc.reference_name; + else + filters["parent"] = doc.reference_name; + return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", - filters: { - "from": doctype, - "parent": doc.reference_name, - "inspection_type": doc.inspection_type - } + filters: filters }; } }, diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index dd95075e28..f6d76194d9 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -73,7 +73,7 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry", + "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry\nJob Card", "reqd": 1 }, { @@ -236,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-21 13:03:11.938072", + "modified": "2020-11-19 17:06:05.409963", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 399a63a186..ae4eb9b995 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -53,16 +53,28 @@ class QualityInspection(Document): def update_qc_reference(self): quality_inspection = self.name if self.docstatus == 1 else "" - doctype = self.reference_type + ' Item' - if self.reference_type == 'Stock Entry': - doctype = 'Stock Entry Detail' - if self.reference_type and self.reference_name: - frappe.db.sql("""update `tab{child_doc}` t1, `tab{parent_doc}` t2 - set t1.quality_inspection = %s, t2.modified = %s - where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""" - .format(parent_doc=self.reference_type, child_doc=doctype), - (quality_inspection, self.modified, self.reference_name, self.item_code)) + if self.reference_type == 'Job Card': + if self.reference_name: + frappe.db.sql(""" + UPDATE `tab{doctype}` + SET quality_inspection = %s, modified = %s + WHERE name = %s and production_item = %s + """.format(doctype=self.reference_type), + (quality_inspection, self.modified, self.reference_name, self.item_code)) + + else: + doctype = self.reference_type + ' Item' + if self.reference_type == 'Stock Entry': + doctype = 'Stock Entry Detail' + + if self.reference_type and self.reference_name: + frappe.db.sql(""" + UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 + SET t1.quality_inspection = %s, t2.modified = %s + WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name + """.format(parent_doc=self.reference_type, child_doc=doctype), + (quality_inspection, self.modified, self.reference_name, self.item_code)) def set_status_based_on_acceptance_formula(self): for reading in self.readings: @@ -95,27 +107,44 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): mcond = get_match_cond(filters["from"]) cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" - if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\ - and filters.get("inspection_type") != "In Process": - cond = """and item_code in (select name from `tabItem` where - inspection_required_before_purchase = 1)""" - elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\ - and filters.get("inspection_type") != "In Process": - cond = """and item_code in (select name from `tabItem` where - inspection_required_before_delivery = 1)""" - elif filters.get('from') == 'Stock Entry Detail': - cond = """and s_warehouse is null""" + if filters.get("parent"): + if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\ + and filters.get("inspection_type") != "In Process": + cond = """and item_code in (select name from `tabItem` where + inspection_required_before_purchase = 1)""" + elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\ + and filters.get("inspection_type") != "In Process": + cond = """and item_code in (select name from `tabItem` where + inspection_required_before_delivery = 1)""" + elif filters.get('from') == 'Stock Entry Detail': + cond = """and s_warehouse is null""" - if filters.get('from') in ['Supplier Quotation Item']: - qi_condition = "" + if filters.get('from') in ['Supplier Quotation Item']: + qi_condition = "" - return frappe.db.sql(""" select item_code from `tab{doc}` - where parent=%(parent)s and docstatus < 2 and item_code like %(txt)s - {qi_condition} {cond} {mcond} - order by item_code limit {start}, {page_len}""".format(doc=filters.get('from'), - parent=filters.get('parent'), cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) + return frappe.db.sql(""" + SELECT item_code + FROM `tab{doc}` + WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s + {qi_condition} {cond} {mcond} + ORDER BY item_code limit {start}, {page_len} + """.format(doc=filters.get('from'), + cond = cond, mcond = mcond, start = start, + page_len = page_len, qi_condition = qi_condition), + {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) + + elif filters.get("reference_name"): + return frappe.db.sql(""" + SELECT production_item + FROM `tab{doc}` + WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s + {qi_condition} {cond} {mcond} + ORDER BY production_item + LIMIT {start}, {page_len} + """.format(doc=filters.get("from"), + cond = cond, mcond = mcond, start = start, + page_len = page_len, qi_condition = qi_condition), + {'reference_name': filters.get('reference_name'), 'txt': "%%%s%%" % txt}) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 7824e812980df8e772c4cf0af90c33ad741585a6 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 25 Nov 2020 14:54:50 +0530 Subject: [PATCH 200/229] fix: ignore exception during leave ledger creation from patch --- .../doctype/leave_application/leave_application.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 3f25f58383..e9bcfb3a8b 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -376,24 +376,32 @@ class LeaveApplication(Document): if expiry_date: self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp) else: + raise_exception = True + if frappe.flags.in_patch: + raise_exception=False + args = dict( leaves=self.total_leave_days * -1, from_date=self.from_date, to_date=self.to_date, is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee) + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' ) create_leave_ledger_entry(self, args, submit) def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): ''' splits leave application into two ledger entries to consider expiry of allocation ''' + + raise_exception = True + if frappe.flags.in_patch: + raise_exception=False + args = dict( from_date=self.from_date, to_date=expiry_date, leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee), - + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' ) create_leave_ledger_entry(self, args, submit) From e15b6a91dea531c555f4e60c6929c7740d3a7422 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 25 Nov 2020 15:36:41 +0530 Subject: [PATCH 201/229] Filters for tax templates (#23998) * feat: add company filter to tax templates * fix: remove filer from PO because it is from tran * fix: linting * fix: solve translation string issues * fix: remove doctype name --- erpnext/accounts/doctype/pos_profile/pos_profile.js | 9 +++++++++ erpnext/public/js/controllers/transaction.js | 11 +++++++++++ erpnext/selling/sales_common.js | 12 +----------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 558e21c13a..7f4f755480 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -35,6 +35,15 @@ frappe.ui.form.on('POS Profile', { }; }); + frm.set_query("taxes_and_charges", function() { + return { + filters: [ + ['Sales Taxes and Charges Template', 'company', '=', frm.doc.company], + ['Sales Taxes and Charges Template', 'docstatus', '!=', 2] + ] + }; + }); + frm.set_query('company_address', function(doc) { if(!doc.company) { frappe.throw(__('Please set Company')); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1358a4bd08..7f08cd1359 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -209,6 +209,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); } + if (this.frm.fields_dict.taxes_and_charges) { + this.frm.set_query("taxes_and_charges", function() { + return { + filters: [ + ['company', '=', me.frm.doc.company], + ['docstatus', '!=', 2] + ] + }; + }); + } + }, onload: function() { var me = this; diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 002cfe41e1..7f00fca8f0 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -42,16 +42,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ me.frm.set_query('customer_address', erpnext.queries.address_query); me.frm.set_query('shipping_address_name', erpnext.queries.address_query); - if(this.frm.fields_dict.taxes_and_charges) { - this.frm.set_query("taxes_and_charges", function() { - return { - filters: [ - ['Sales Taxes and Charges Template', 'company', '=', me.frm.doc.company], - ['Sales Taxes and Charges Template', 'docstatus', '!=', 2] - ] - } - }); - } if(this.frm.fields_dict.selling_price_list) { this.frm.set_query("selling_price_list", function() { @@ -479,7 +469,7 @@ frappe.ui.form.on(cur_frm.doctype,"project", function(frm) { $.each(frm.doc["items"] || [], function(i, row) { if(r.message) { frappe.model.set_value(row.doctype, row.name, "cost_center", r.message); - frappe.msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message)); + frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message])); } }) } From e60a62bde5b59c9d067d220d20aa4346795816ff Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Wed, 25 Nov 2020 15:37:02 +0530 Subject: [PATCH 202/229] fix: function imports in account_balance_timeline.py (#24003) --- .../account_balance_timeline/account_balance_timeline.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 39bf4b053a..85f54f98ba 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -6,9 +6,8 @@ import frappe, json from frappe import _ from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form from erpnext.accounts.report.general_ledger.general_ledger import execute -from frappe.utils.dashboard import cache_source, get_from_date_from_timespan -from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending - +from frappe.utils.dashboard import cache_source +from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending from frappe.utils.nestedset import get_descendants_of @frappe.whitelist() From 90e33e53fd649f5a52461a9403106aab4b89aeed Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 25 Nov 2020 15:37:54 +0530 Subject: [PATCH 203/229] refactor: Format translation strings (#24004) * fix: translation strings * fix: linting --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +-- .../doctype/clinical_procedure/clinical_procedure.js | 6 ++---- erpnext/public/js/controllers/buying.js | 3 +-- erpnext/selling/doctype/sales_order/sales_order.js | 3 +-- erpnext/selling/page/point_of_sale/pos_controller.js | 3 +-- erpnext/setup/doctype/sales_person/sales_person.js | 3 +-- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d8394785c6..ab4bfb14ec 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -339,8 +339,7 @@ class JournalEntry(AccountsController): currency=account_currency) if flt(voucher_total) < (flt(order.advance_paid) + total): - frappe.throw(_("Advance paid against {0} {1} cannot be greater \ - than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total)) + frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total)) def validate_invoices(self): """Validate totals and docstatus for invoices""" diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js index eb7d4bdeba..1d4411d73d 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js @@ -85,8 +85,7 @@ frappe.ui.form.on('Clinical Procedure', { callback: function(r) { if (r.message) { frappe.show_alert({ - message: __('Stock Entry {0} created', - ['' + r.message + '']), + message: __('Stock Entry {0} created', ['' + r.message + '']), indicator: 'green' }); } @@ -105,8 +104,7 @@ frappe.ui.form.on('Clinical Procedure', { callback: function(r) { if (!r.exc) { if (r.message == 'insufficient stock') { - let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', - [frm.doc.warehouse.bold()]); + let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', [frm.doc.warehouse.bold()]); frappe.confirm( msg, function() { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 58ac38f0a8..3f5652aa5d 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -218,8 +218,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ var is_negative_qty = false; for(var i = 0; i%(name)s', {name:d}) }).join(', ')]), indicator: 'green' diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 970d840665..ad1633e71d 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -644,8 +644,7 @@ erpnext.PointOfSale.Controller = class { }) } else if (available_qty < qty_needed) { frappe.show_alert({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', - [bold_item_code, bold_warehouse, bold_available_qty]), + message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' }); frappe.utils.play_sound("error"); diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index 8f7593d6ee..b71a92f8a9 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -5,8 +5,7 @@ frappe.ui.form.on('Sales Person', { refresh: function(frm) { if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { var info = frm.doc.__onload.dashboard_info; - frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', - [format_currency(info.allocated_amount, info.currency)]), 'blue'); + frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue'); } }, From f32cff1080f9412ed27a843d1f573021d56d5db5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 25 Nov 2020 16:00:15 +0530 Subject: [PATCH 204/229] feat : Leave type with partial payment (#23173) * feat: Partially paid Leaves * feat: some importatnt validation * fix: requested changes * fix: requested changes * fix: travis, sider, codacy * fix: changes requested * test: Partially Paid Leaves --- erpnext/hr/doctype/employee/employee.json | 4 +- .../leave_application/leave_application.py | 6 +- erpnext/hr/doctype/leave_type/leave_type.json | 19 +++++- erpnext/hr/doctype/leave_type/leave_type.py | 6 ++ .../hr/doctype/leave_type/test_leave_type.py | 5 ++ .../doctype/salary_slip/salary_slip.js | 42 ++++++------ .../doctype/salary_slip/salary_slip.py | 64 +++++++++++++------ .../doctype/salary_slip/test_salary_slip.py | 19 +++++- 8 files changed, 117 insertions(+), 48 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index da789198e5..4cabe97cc4 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -672,10 +672,10 @@ "oldfieldtype": "Date" }, { - "depends_on": "eval:doc.status == \"Left\"", "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", + "mandatory_depends_on": "eval:doc.status == \"Left\"", "oldfieldname": "relieving_date", "oldfieldtype": "Date" }, @@ -822,7 +822,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-06 15:58:23.805489", + "modified": "2020-10-16 14:41:10.580897", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 3f25f58383..ca79dff115 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -130,8 +130,7 @@ class LeaveApplication(Document): if self.status == "Approved": for dt in daterange(getdate(self.from_date), getdate(self.to_date)): date = dt.strftime("%Y-%m-%d") - status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave" - + status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave" attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee, attendance_date = date, docstatus = ('!=', 2))) @@ -293,7 +292,8 @@ class LeaveApplication(Document): def set_half_day_date(self): if self.from_date == self.to_date and self.half_day == 1: self.half_day_date = self.from_date - elif self.half_day == 0: + + if self.half_day == 0: self.half_day_date = None def notify_employee(self): diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 0af832f903..4a135e0ffe 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -15,6 +15,8 @@ "column_break_3", "is_carry_forward", "is_lwp", + "is_ppl", + "fraction_of_daily_salary_per_leave", "is_optional_leave", "allow_negative", "include_holiday", @@ -77,6 +79,7 @@ }, { "default": "0", + "depends_on": "eval:doc.is_ppl == 0", "fieldname": "is_lwp", "fieldtype": "Check", "label": "Is Leave Without Pay" @@ -183,12 +186,26 @@ { "fieldname": "column_break_22", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.is_lwp == 0", + "fieldname": "is_ppl", + "fieldtype": "Check", + "label": "Is Partially Paid Leave" + }, + { + "depends_on": "eval:doc.is_ppl == 1", + "fieldname": "fraction_of_daily_salary_per_leave", + "fieldtype": "Float", + "label": "Fraction of Daily Salary per Leave", + "mandatory_depends_on": "eval:doc.is_ppl == 1" } ], "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2019-12-12 12:48:37.780254", + "modified": "2020-08-26 14:04:54.318687", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py index c0d1296841..21f180b857 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.py +++ b/erpnext/hr/doctype/leave_type/leave_type.py @@ -21,3 +21,9 @@ class LeaveType(Document): leave_allocation = [l['name'] for l in leave_allocation] if leave_allocation: frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec + + if self.is_lwp and self.is_ppl: + frappe.throw(_("Leave Type can be either without pay or partial pay")) + + if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1): + frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1")) diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index 0c4f435860..7fef2975c8 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -18,9 +18,14 @@ def create_leave_type(**args): "allow_encashment": args.allow_encashment or 0, "is_earned_leave": args.is_earned_leave or 0, "is_lwp": args.is_lwp or 0, + "is_ppl":args.is_ppl or 0, "is_carry_forward": args.is_carry_forward or 0, "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, "encashment_threshold_days": args.encashment_threshold_days or 5, "earning_component": "Leave Encashment" }) + + if leave_type.is_ppl: + leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 + return leave_type \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 7b69dbe8d6..0671b570d1 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -13,12 +13,12 @@ frappe.ui.form.on("Salary Slip", { ]; }); - frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(){ + frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function() { return { filters: { employee: frm.doc.employee } - } + }; }; frm.set_query("salary_component", "earnings", function() { @@ -26,7 +26,7 @@ frappe.ui.form.on("Salary Slip", { filters: { type: "earning" } - } + }; }); frm.set_query("salary_component", "deductions", function() { @@ -34,18 +34,18 @@ frappe.ui.form.on("Salary Slip", { filters: { type: "deduction" } - } + }; }); frm.set_query("employee", function() { - return{ + return { query: "erpnext.controllers.queries.employee_query" - } + }; }); }, - start_date: function(frm){ - if(frm.doc.start_date){ + start_date: function(frm) { + if (frm.doc.start_date) { frm.trigger("set_end_date"); } }, @@ -54,7 +54,7 @@ frappe.ui.form.on("Salary Slip", { frm.events.get_emp_and_working_day_details(frm); }, - set_end_date: function(frm){ + set_end_date: function(frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -66,22 +66,22 @@ frappe.ui.form.on("Salary Slip", { frm.set_value('end_date', r.message.end_date); } } - }) + }); }, company: function(frm) { var company = locals[':Company'][frm.doc.company]; - if(!frm.doc.letter_head && company.default_letter_head) { + if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } }, refresh: function(frm) { - frm.trigger("toggle_fields") + frm.trigger("toggle_fields"); var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; - cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields,false); - cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields,false); + cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); + cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); }, salary_slip_based_on_timesheet: function(frm) { @@ -98,12 +98,12 @@ frappe.ui.form.on("Salary Slip", { frm.events.get_emp_and_working_day_details(frm); }, - leave_without_pay: function(frm){ + leave_without_pay: function(frm) { if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) { return frappe.call({ method: 'process_salary_based_on_working_days', doc: frm.doc, - callback: function(r, rt) { + callback: function() { frm.refresh(); } }); @@ -121,10 +121,10 @@ frappe.ui.form.on("Salary Slip", { return frappe.call({ method: 'get_emp_and_working_day_details', doc: frm.doc, - callback: function(r, rt) { + callback: function(r) { frm.refresh(); - if (r.message){ - frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); + if (r.message[1] !== "Leave" && r.message[0]) { + frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as ")+ r.message[0] +__(". You can can change this in ") + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); } } }); @@ -141,7 +141,7 @@ frappe.ui.form.on('Salary Slip Timesheet', { }); // calculate total working hours, earnings based on hourly wages and totals -var total_work_hours = function(frm, dt, dn) { +var total_work_hours = function(frm) { var total_working_hours = 0.0; $.each(frm.doc["timesheets"] || [], function(i, timesheet) { total_working_hours += timesheet.working_hours; @@ -165,4 +165,4 @@ var total_work_hours = function(frm, dt, dn) { frm.doc.rounded_total = Math.round(frm.doc.net_pay); refresh_many(['net_pay', 'rounded_total']); }); -} +}; diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index cecb8cde7c..7b87ae5e7b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -136,8 +136,8 @@ class SalarySlip(TransactionBase): self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.set_time_sheet() self.pull_sal_struct() - consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" - return consider_unmarked_attendance_as + payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) + return [payroll_based_on, consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: @@ -210,10 +210,10 @@ class SalarySlip(TransactionBase): frappe.throw(_("Please set Payroll based on in Payroll settings")) if payroll_based_on == "Attendance": - actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays) + actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays) self.absent_days = absent else: - actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) + actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days) if not lwp: lwp = actual_lwp @@ -300,7 +300,7 @@ class SalarySlip(TransactionBase): return holidays - def calculate_lwp_based_on_leave_application(self, holidays, working_days): + def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) daily_wages_fraction_for_half_day = \ @@ -311,10 +311,12 @@ class SalarySlip(TransactionBase): leave = frappe.db.sql(""" SELECT t1.name, CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) - THEN t1.half_day else 0 END + THEN t1.half_day else 0 END, + t2.is_ppl, + t2.fraction_of_daily_salary_per_leave FROM `tabLeave Application` t1, `tabLeave Type` t2 WHERE t2.name = t1.leave_type - AND t2.is_lwp = 1 + AND (t2.is_lwp = 1 or t2.is_ppl = 1) AND t1.docstatus = 1 AND t1.employee = %(employee)s AND ifnull(t1.salary_slip, '') = '' @@ -327,19 +329,35 @@ class SalarySlip(TransactionBase): """.format(holidays), {"employee": self.employee, "dt": dt}) if leave: + equivalent_lwp_count = 0 is_half_day_leave = cint(leave[0][1]) - lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + is_partially_paid_leave = cint(leave[0][2]) + fraction_of_daily_salary_per_leave = flt(leave[0][3]) + + equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + + if is_partially_paid_leave: + equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + + lwp += equivalent_lwp_count return lwp - def calculate_lwp_and_absent_days_based_on_attendance(self, holidays): + def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays): lwp = 0 absent = 0 daily_wages_fraction_for_half_day = \ flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 - lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) + leave_types = frappe.get_all("Leave Type", + or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]], + fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"]) + + leave_type_map = {} + for leave_type in leave_types: + leave_type_map[leave_type.name] = leave_type + attendances = frappe.db.sql(''' SELECT attendance_date, status, leave_type FROM `tabAttendance` @@ -351,21 +369,30 @@ class SalarySlip(TransactionBase): ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) for d in attendances: - if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: + if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys(): continue if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: if d.status == "Absent" or \ - (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): + (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']): continue + if d.leave_type: + fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"] + if d.status == "Half Day": - lwp += (1 - daily_wages_fraction_for_half_day) - elif d.status == "On Leave" and d.leave_type in lwp_leave_types: - lwp += 1 + equivalent_lwp = (1 - daily_wages_fraction_for_half_day) + + if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]: + equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + lwp += equivalent_lwp + elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys(): + equivalent_lwp = 1 + if leave_type_map[d.leave_type]["is_ppl"]: + equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + lwp += equivalent_lwp elif d.status == "Absent": absent += 1 - return lwp, absent def add_earning_for_hourly_wages(self, doc, salary_component, amount): @@ -949,9 +976,8 @@ class SalarySlip(TransactionBase): amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") total_amount = amounts['interest_amount'] + amounts['payable_principal_amount'] if payment.total_payment > total_amount: - frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} - against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment), - frappe.bold(total_amount), frappe.bold(payment.loan))) + frappe.throw(_("Row {0}: Paid amount {1} is greater than pending accrued amount {2}against loan {3}").format( + payment.idx, frappe.bold(payment.total_payment),frappe.bold(total_amount), frappe.bold(payment.loan))) self.total_interest_amount += payment.interest_amount self.total_principal_amount += payment.principal_amount diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 7fe4165362..e08dc7c9c8 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -13,6 +13,8 @@ from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \ import create_payroll_period, create_exemption_category @@ -93,14 +95,27 @@ class TestSalarySlip(unittest.TestCase): make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") + leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1) + leave_type_ppl.save() + + alloc = create_leave_allocation( + employee = emp_id, from_date = add_days(first_sunday, 4), + to_date = add_days(first_sunday, 10), new_leaves_allocated = 3, + leave_type = "Test Partially Paid Leave") + alloc.save() + alloc.submit() + + #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp + make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave") + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") - self.assertEqual(ss.leave_without_pay, 3) + self.assertEqual(ss.leave_without_pay, 4) days_in_month = no_of_days[0] no_of_holidays = no_of_days[1] - self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3) + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) #Gross pay calculation based on attendances gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) From 755b773616cb7e037e2034d2bc0e396f36580a3f Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 25 Nov 2020 16:05:17 +0530 Subject: [PATCH 205/229] feat: Leave policy assignment (#23112) * feat: Leave Policy Assignment * feat: linking with leave allocation and valiations * style: removed old code from leave period * feat: Bulk Leave policy Assignment and grant Leaves * fix: overlap validation * feat: earned leaves based on joining date * feat: automatic grant leave based on leave policy * patch: create leave policy assignment based on employee current leave policy * fix: dependent test cases * test: Leave policy assignment * fix: some enhancement * style: break large function into small function * fix:requested Changes * fix(patch): Handled old Leave allocatioln * fix:codacy * fix: travis and sider,codacy * fix: codacy * fix: codacy * fix: requested changes and sider Co-authored-by: Nabin Hait --- erpnext/hooks.py | 4 +- erpnext/hr/doctype/employee/employee.json | 11 +- .../employee_grade/employee_grade.json | 130 ++------------ .../hr/doctype/hr_settings/hr_settings.json | 13 +- .../leave_allocation/leave_allocation.json | 16 +- .../leave_allocation/leave_allocation.py | 10 ++ .../test_leave_application.py | 37 ++-- .../leave_encashment/test_leave_encashment.py | 18 +- .../hr/doctype/leave_period/leave_period.js | 78 +-------- .../hr/doctype/leave_period/leave_period.py | 109 +----------- .../doctype/leave_period/test_leave_period.py | 34 +--- .../leave_policy_assignment/__init__.py | 0 .../leave_policy_assignment.js | 72 ++++++++ .../leave_policy_assignment.json | 160 +++++++++++++++++ .../leave_policy_assignment.py | 163 ++++++++++++++++++ .../leave_policy_assignment_list.js | 138 +++++++++++++++ .../test_leave_policy_assignment.py | 103 +++++++++++ erpnext/hr/doctype/leave_type/leave_type.json | 10 +- erpnext/hr/utils.py | 129 ++++++++------ erpnext/patches.txt | 1 + ..._based_on_employee_current_leave_policy.py | 77 +++++++++ 21 files changed, 899 insertions(+), 414 deletions(-) create mode 100644 erpnext/hr/doctype/leave_policy_assignment/__init__.py create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js create mode 100644 erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py create mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 741176f33f..726ab6e22a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -347,14 +347,16 @@ scheduler_events = { "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", + "erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy", "erpnext.hr.utils.generate_leave_encashment", + "erpnext.hr.utils.allocate_earned_leaves", + "erpnext.hr.utils.grant_leaves_automatically", "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.doctype.lead.lead.daily_open_lead" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", - "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" ] } diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4cabe97cc4..4f1c04ff5d 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -57,7 +57,6 @@ "column_break_45", "shift_request_approver", "attendance_and_leave_details", - "leave_policy", "attendance_device_id", "column_break_44", "holiday_list", @@ -411,14 +410,6 @@ "oldfieldtype": "Link", "options": "Branch" }, - { - "fetch_from": "grade.default_leave_policy", - "fetch_if_empty": 1, - "fieldname": "leave_policy", - "fieldtype": "Link", - "label": "Leave Policy", - "options": "Leave Policy" - }, { "description": "Applicable Holiday List", "fieldname": "holiday_list", @@ -822,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 14:41:10.580897", + "modified": "2020-10-16 15:02:04.283657", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee_grade/employee_grade.json b/erpnext/hr/doctype/employee_grade/employee_grade.json index e63ffae0c4..88b061a3c3 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade.json +++ b/erpnext/hr/doctype/employee_grade/employee_grade.json @@ -1,167 +1,69 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 16:14:24.174138", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 16:14:24.174138", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "default_salary_structure" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_leave_policy", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Leave Policy", - "length": 0, - "no_copy": 0, - "options": "Leave Policy", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "default_salary_structure", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Default Salary Structure", - "length": 0, - "no_copy": 0, - "options": "Salary Structure", - "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 + "options": "Salary Structure" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 17:17:45.617624", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-26 13:12:07.815330", "modified_by": "Administrator", "module": "HR", "name": "Employee Grade", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "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": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 4374d2911a..f99963504a 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -21,6 +21,7 @@ "show_leaves_of_all_department_members_in_calendar", "auto_leave_encashment", "restrict_backdated_leave_application", + "automatically_allocate_leaves_based_on_leave_policy", "hiring_settings", "check_vacancies" ], @@ -41,7 +42,7 @@ "description": "Employee records are created using the selected field", "fieldname": "emp_created_by", "fieldtype": "Select", - "label": "Employee Records to Be Created By", + "label": "Employee Records to be created by", "options": "Naming Series\nEmployee Number\nFull Name" }, { @@ -117,7 +118,7 @@ "default": "0", "fieldname": "restrict_backdated_leave_application", "fieldtype": "Check", - "label": "Restrict Backdated Leave Applications" + "label": "Restrict Backdated Leave Application" }, { "depends_on": "eval:doc.restrict_backdated_leave_application == 1", @@ -125,13 +126,19 @@ "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", "options": "Role" + }, + { + "default": "0", + "fieldname": "automatically_allocate_leaves_based_on_leave_policy", + "fieldtype": "Check", + "label": "Automatically Allocate Leaves Based On Leave Policy" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 11:49:46.168027", + "modified": "2020-08-27 14:30:28.995324", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 007497e34a..4b315014da 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-02-20 19:10:38", @@ -24,6 +25,7 @@ "compensatory_request", "leave_period", "leave_policy", + "leave_policy_assignment", "carry_forwarded_leaves_count", "expired", "amended_from", @@ -160,9 +162,10 @@ "read_only": 1 }, { - "fetch_from": "employee.leave_policy", + "fetch_from": "leave_policy_assignment.leave_policy", "fieldname": "leave_policy", "fieldtype": "Link", + "hidden": 1, "in_standard_filter": 1, "label": "Leave Policy", "options": "Leave Policy", @@ -209,12 +212,21 @@ "fieldtype": "Float", "label": "Carry Forwarded Leaves", "read_only": 1 + }, + { + "fieldname": "leave_policy_assignment", + "fieldtype": "Link", + "label": "Leave Policy Assignment", + "options": "Leave Policy Assignment", + "read_only": 1 } ], "icon": "fa fa-ok", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-08-08 15:08:42.440909", + "links": [], + "modified": "2020-08-20 14:25:10.314323", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 03fe3fa035..a09cd2ea11 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -51,9 +51,19 @@ class LeaveAllocation(Document): def on_cancel(self): self.create_leave_ledger_entry(submit=False) + if self.leave_policy_assignment: + self.update_leave_policy_assignments_when_no_allocations_left() if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def update_leave_policy_assignments_when_no_allocations_left(self): + allocations = frappe.db.get_list("Leave Allocation", filters = { + "docstatus": 1, + "leave_policy_assignment": self.leave_policy_assignment + }) + if len(allocations) == 0: + frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0) + def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: frappe.throw(_("To date cannot be before from date")) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 6e909c3f01..53b7a39e51 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -10,6 +10,7 @@ from frappe.permissions import clear_user_permissions_for_doctype from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees test_dependencies = ["Leave Allocation", "Leave Block List"] @@ -410,25 +411,39 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) def test_earned_leaves_creation(self): + + frappe.db.sql('''delete from `tabLeave Period`''') + frappe.db.sql('''delete from `tabLeave Policy Assignment`''') + frappe.db.sql('''delete from `tabLeave Allocation`''') + frappe.db.sql('''delete from `tabLeave Ledger Entry`''') + leave_period = get_leave_period() employee = get_employee() leave_type = 'Test Earned Leave Type' - if not frappe.db.exists('Leave Type', leave_type): - frappe.get_doc(dict( - leave_type_name = leave_type, - doctype = 'Leave Type', - is_earned_leave = 1, - earned_leave_frequency = 'Monthly', - rounding = 0.5, - max_leaves_allowed = 6 - )).insert() + frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1) + frappe.get_doc(dict( + leave_type_name = leave_type, + doctype = 'Leave Type', + is_earned_leave = 1, + earned_leave_frequency = 'Monthly', + rounding = 0.5, + max_leaves_allowed = 6 + )).insert() + leave_policy = frappe.get_doc({ "doctype": "Leave Policy", "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}] }).insert() - frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name) - allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() from erpnext.hr.utils import allocate_earned_leaves i = 0 diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 99f6463416..bbee18bb0a 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -9,6 +9,7 @@ from frappe.utils import today, add_months from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\ test_dependencies = ["Leave Type"] @@ -16,6 +17,7 @@ test_dependencies = ["Leave Type"] class TestLeaveEncashment(unittest.TestCase): def setUp(self): frappe.db.sql('''delete from `tabLeave Period`''') + frappe.db.sql('''delete from `tabLeave Policy Assignment`''') frappe.db.sql('''delete from `tabLeave Allocation`''') frappe.db.sql('''delete from `tabLeave Ledger Entry`''') frappe.db.sql('''delete from `tabAdditional Salary`''') @@ -29,14 +31,22 @@ class TestLeaveEncashment(unittest.TestCase): # create employee, salary structure and assignment self.employee = make_employee("test_employee_encashment@example.com") - frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name) + self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": self.leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data)) salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee, other_details={"leave_encashment_amount_per_day": 50}) - # create the leave period and assign the leaves - self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - self.leave_period.grant_leave_allocation(employee=self.employee) + #grant Leaves + frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() + def test_leave_balance_value_and_amount(self): frappe.db.sql('''delete from `tabLeave Encashment`''') diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js index bad2b8766c..0e88bc1671 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.js +++ b/erpnext/hr/doctype/leave_period/leave_period.js @@ -2,14 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Leave Period', { - refresh: (frm)=>{ - frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0); - if(!frm.is_new()) { - frm.add_custom_button(__('Grant Leaves'), function () { - frm.trigger("grant_leaves"); - }); - } - }, from_date: (frm)=>{ if (frm.doc.from_date && !frm.doc.to_date) { var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); @@ -22,73 +14,7 @@ frappe.ui.form.on('Leave Period', { "filters": { "company": frm.doc.company, } - } - }) - }, - grant_leaves: function(frm) { - var d = new frappe.ui.Dialog({ - title: __('Grant Leaves'), - fields: [ - { - "label": "Filter Employees By (Optional)", - "fieldname": "sec_break", - "fieldtype": "Section Break", - }, - { - "label": "Employee Grade", - "fieldname": "grade", - "fieldtype": "Link", - "options": "Employee Grade" - }, - { - "label": "Department", - "fieldname": "department", - "fieldtype": "Link", - "options": "Department" - }, - { - "fieldname": "col_break", - "fieldtype": "Column Break", - }, - { - "label": "Designation", - "fieldname": "designation", - "fieldtype": "Link", - "options": "Designation" - }, - { - "label": "Employee", - "fieldname": "employee", - "fieldtype": "Link", - "options": "Employee" - }, - { - "fieldname": "sec_break", - "fieldtype": "Section Break", - }, - { - "label": "Add unused leaves from previous allocations", - "fieldname": "carry_forward", - "fieldtype": "Check" - } - ], - primary_action: function() { - var data = d.get_values(); - - frappe.call({ - doc: frm.doc, - method: "grant_leave_allocation", - args: data, - callback: function(r) { - if(!r.exc) { - d.hide(); - frm.reload_doc(); - } - } - }); - }, - primary_action_label: __('Grant') + }; }); - d.show(); - } + }, }); diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index 0973ac7198..28a33f6fac 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -7,24 +7,10 @@ import frappe from frappe import _ from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil from frappe.model.document import Document -from erpnext.hr.utils import validate_overlap, get_employee_leave_policy +from erpnext.hr.utils import validate_overlap from frappe.utils.background_jobs import enqueue -from six import iteritems class LeavePeriod(Document): - def get_employees(self, args): - conditions, values = [], [] - for field, value in iteritems(args): - if value: - conditions.append("{0}=%s".format(field)) - values.append(value) - - condition_str = " and " + " and ".join(conditions) if len(conditions) else "" - - employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec - .format(condition=condition_str), tuple(values))) - - return employees def validate(self): self.validate_dates() @@ -33,96 +19,3 @@ class LeavePeriod(Document): def validate_dates(self): if getdate(self.from_date) >= getdate(self.to_date): frappe.throw(_("To date can not be equal or less than from date")) - - - def grant_leave_allocation(self, grade=None, department=None, designation=None, - employee=None, carry_forward=0): - employee_records = self.get_employees({ - "grade": grade, - "department": department, - "designation": designation, - "name": employee - }) - - if employee_records: - if len(employee_records) > 20: - frappe.enqueue(grant_leave_alloc_for_employees, timeout=600, - employee_records=employee_records, leave_period=self, carry_forward=carry_forward) - else: - grant_leave_alloc_for_employees(employee_records, self, carry_forward) - else: - frappe.msgprint(_("No Employee Found")) - -def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0): - leave_allocations = [] - existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name) - leave_type_details = get_leave_type_details() - count = 0 - for employee in employee_records.keys(): - if employee in existing_allocations_for: - continue - count +=1 - leave_policy = get_employee_leave_policy(employee) - if leave_policy: - for leave_policy_detail in leave_policy.leave_policy_details: - if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: - leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type, - leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee)) - leave_allocations.append(leave_allocation) - frappe.db.commit() - frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves...")) - - if leave_allocations: - frappe.msgprint(_("Leaves has been granted sucessfully")) - -def get_existing_allocations(employees, leave_period): - leave_allocations = frappe.db.sql_list(""" - SELECT DISTINCT - employee - FROM `tabLeave Allocation` - WHERE - leave_period=%s - AND employee in (%s) - AND carry_forward=0 - AND docstatus=1 - """ % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees) - if leave_allocations: - frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}") - .format("\n".join(leave_allocations))) - return leave_allocations - -def get_leave_type_details(): - leave_type_details = frappe._dict() - leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"]) - for d in leave_types: - leave_type_details.setdefault(d.name, d) - return leave_type_details - -def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining): - ''' Creates leave allocation for the given employee in the provided leave period ''' - if carry_forward and not leave_type_details.get(leave_type).is_carry_forward: - carry_forward = 0 - - # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period - if getdate(date_of_joining) > getdate(leave_period.from_date): - remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1)) - new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) - - # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 - if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: - new_leaves_allocated = 0 - - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=employee, - leave_type=leave_type, - from_date=leave_period.from_date, - to_date=leave_period.to_date, - new_leaves_allocated=new_leaves_allocated, - leave_period=leave_period.name, - carry_forward=carry_forward - )) - allocation.save(ignore_permissions = True) - allocation.submit() - return allocation.name \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 1762cf917a..b5857bcd8f 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -5,43 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext import unittest -from frappe.utils import today, add_months -from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on test_dependencies = ["Employee", "Leave Type", "Leave Policy"] class TestLeavePeriod(unittest.TestCase): - def setUp(self): - frappe.db.sql("delete from `tabLeave Period`") - - def test_leave_grant(self): - leave_type = "_Test Leave Type" - - # create the leave policy - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": leave_type, - "annual_allocation": 20 - }] - }).insert() - leave_policy.submit() - - # create employee and assign the leave period - employee = "test_leave_period@employee.com" - employee_doc_name = make_employee(employee) - frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name) - - # clear the already allocated leave - frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com") - - # create the leave period - leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - - # test leave_allocation - leave_period.grant_leave_allocation(employee=employee_doc_name) - self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) + pass def create_leave_period(from_date, to_date, company=None): leave_period = frappe.db.get_value('Leave Period', diff --git a/erpnext/hr/doctype/leave_policy_assignment/__init__.py b/erpnext/hr/doctype/leave_policy_assignment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js new file mode 100644 index 0000000000..7c32a0dde0 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js @@ -0,0 +1,72 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Leave Policy Assignment', { + onload: function(frm) { + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + }, + + refresh: function(frm) { + if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) { + frm.add_custom_button(__("Grant Leave"), function() { + + frappe.call({ + doc: frm.doc, + method: "grant_leave_alloc_for_employee", + callback: function(r) { + let leave_allocations = r.message; + let msg = frm.events.get_success_message(leave_allocations); + frappe.msgprint(msg); + cur_frm.refresh(); + } + }); + }); + } + }, + + get_success_message: function(leave_allocations) { + let msg = __("Leaves has been granted successfully"); + msg += "
"; + msg += ""; + for (let key in leave_allocations) { + msg += ""; + } + msg += "
"+__('Leave Type')+""+__("Leave Allocation")+""+__("Leaves Granted")+"
"+key+""+leave_allocations[key]["name"]+""+leave_allocations[key]["leaves"]+"
"; + return msg; + }, + + assignment_based_on: function(frm) { + if (frm.doc.assignment_based_on) { + frm.events.set_effective_date(frm); + } else { + frm.set_value("effective_from", ''); + frm.set_value("effective_to", ''); + } + }, + + leave_period: function(frm) { + if (frm.doc.leave_period) { + frm.events.set_effective_date(frm); + } + }, + + set_effective_date: function(frm) { + if (frm.doc.assignment_based_on == "Leave Period" && frm.doc.leave_period) { + frappe.model.with_doc("Leave Period", frm.doc.leave_period, function () { + let from_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "from_date"); + let to_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "to_date"); + frm.set_value("effective_from", from_date); + frm.set_value("effective_to", to_date); + + }); + } else if (frm.doc.assignment_based_on == "Joining Date" && frm.doc.employee) { + frappe.model.with_doc("Employee", frm.doc.employee, function () { + let from_date = frappe.model.get_value("Employee", frm.doc.employee, "date_of_joining"); + frm.set_value("effective_from", from_date); + frm.set_value("effective_to", frappe.datetime.add_months(frm.doc.effective_from, 12)); + }); + } + frm.refresh(); + } + +}); diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json new file mode 100644 index 0000000000..ecebb3b7d6 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "HR-LPOL-ASSGN-.#####", + "creation": "2020-08-19 13:02:43.343666", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "company", + "leave_policy", + "carry_forward", + "column_break_5", + "assignment_based_on", + "leave_period", + "effective_from", + "effective_to", + "leaves_allocated", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee name", + "read_only": 1 + }, + { + "fieldname": "leave_policy", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Leave Policy", + "options": "Leave Policy", + "reqd": 1 + }, + { + "fieldname": "assignment_based_on", + "fieldtype": "Select", + "label": "Assignment based on", + "options": "\nLeave Period\nJoining Date" + }, + { + "depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "fieldname": "leave_period", + "fieldtype": "Link", + "label": "Leave Period", + "mandatory_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "options": "Leave Period" + }, + { + "fieldname": "effective_from", + "fieldtype": "Date", + "label": "Effective From", + "read_only_depends_on": "eval:doc.assignment_based_on", + "reqd": 1 + }, + { + "fieldname": "effective_to", + "fieldtype": "Date", + "label": "Effective To", + "read_only_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Leave Policy Assignment", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "carry_forward", + "fieldtype": "Check", + "label": "Add unused leaves from previous allocations" + }, + { + "default": "0", + "fieldname": "leaves_allocated", + "fieldtype": "Check", + "hidden": 1, + "label": "Leaves Allocated" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-10-15 15:18:15.227848", + "modified_by": "Administrator", + "module": "HR", + "name": "Leave Policy Assignment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py new file mode 100644 index 0000000000..a5068bc26d --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate +from math import ceil +import json +from six import string_types + +class LeavePolicyAssignment(Document): + + def validate(self): + self.validate_policy_assignment_overlap() + self.set_dates() + + def set_dates(self): + if self.assignment_based_on == "Leave Period": + self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"]) + elif self.assignment_based_on == "Joining Date": + self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + def validate_policy_assignment_overlap(self): + leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = { + "employee": self.employee, + "name": ("!=", self.name), + "docstatus": 1, + "effective_to": (">=", self.effective_from), + "effective_from": ("<=", self.effective_to) + }) + + if len(leave_policy_assignments): + frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") + .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + + def grant_leave_alloc_for_employee(self): + if self.leaves_allocated: + frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment")) + else: + leave_allocations = {} + leave_type_details = get_leave_type_details() + + leave_policy = frappe.get_doc("Leave Policy", self.leave_policy) + date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + for leave_policy_detail in leave_policy.leave_policy_details: + if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: + leave_allocation, new_leaves_allocated = self.create_leave_allocation( + leave_policy_detail.leave_type, leave_policy_detail.annual_allocation, + leave_type_details, date_of_joining + ) + + leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated} + + self.db_set("leaves_allocated", 1) + return leave_allocations + + def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + # Creates leave allocation for the given employee in the provided leave period + carry_forward = self.carry_forward + if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward: + carry_forward = 0 + + new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated, + leave_type_details, date_of_joining) + + allocation = frappe.get_doc(dict( + doctype="Leave Allocation", + employee=self.employee, + leave_type=leave_type, + from_date=self.effective_from, + to_date=self.effective_to, + new_leaves_allocated=new_leaves_allocated, + leave_period=self.leave_period or None, + leave_policy_assignment = self.name, + leave_policy = self.leave_policy, + carry_forward=carry_forward + )) + allocation.save(ignore_permissions = True) + allocation.submit() + return allocation.name, new_leaves_allocated + + def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period + if getdate(date_of_joining) > getdate(self.effective_from): + remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) + new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) + + # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 + if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: + new_leaves_allocated = 0 + + return new_leaves_allocated + +@frappe.whitelist() +def grant_leave_for_multiple_employees(leave_policy_assignments): + leave_policy_assignments = json.loads(leave_policy_assignments) + not_granted = [] + for assignment in leave_policy_assignments: + try: + frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee() + except Exception: + not_granted.append(assignment) + + if len(not_granted): + msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents") + else: + msg = _("Leave granted Successfully") + frappe.msgprint(msg) + +@frappe.whitelist() +def create_assignment_for_multiple_employees(employees, data): + + if isinstance(employees, string_types): + employees= json.loads(employees) + + if isinstance(data, string_types): + data = frappe._dict(json.loads(data)) + + docs_name = [] + for employee in employees: + assignment = frappe.new_doc("Leave Policy Assignment") + assignment.employee = employee + assignment.assignment_based_on = data.assignment_based_on or None + assignment.leave_policy = data.leave_policy + assignment.effective_from = getdate(data.effective_from) or None + assignment.effective_to = getdate(data.effective_to) or None + assignment.leave_period = data.leave_period or None + assignment.carry_forward = data.carry_forward + + assignment.save() + assignment.submit() + docs_name.append(assignment.name) + return docs_name + + +def automatically_allocate_leaves_based_on_leave_policy(): + today = getdate() + automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value( + 'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy' + ) + + pending_assignments = frappe.get_list( + "Leave Policy Assignment", + filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today} + ) + + if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy: + for assignment in pending_assignments: + frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee() + + +def get_leave_type_details(): + leave_type_details = frappe._dict() + leave_types = frappe.get_all("Leave Type", + fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"]) + for d in leave_types: + leave_type_details.setdefault(d.name, d) + return leave_type_details + diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js new file mode 100644 index 0000000000..468f243885 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -0,0 +1,138 @@ +frappe.listview_settings['Leave Policy Assignment'] = { + onload: function (list_view) { + let me = this; + list_view.page.add_inner_button(__("Bulk Leave Policy Assignment"), function () { + me.dialog = new frappe.ui.form.MultiSelectDialog({ + doctype: "Employee", + target: cur_list, + setters: { + company: '', + department: '', + }, + data_fields: [{ + fieldname: 'leave_policy', + fieldtype: 'Link', + options: 'Leave Policy', + label: __('Leave Policy'), + reqd: 1 + }, + { + fieldname: 'assignment_based_on', + fieldtype: 'Select', + options: ["", "Leave Period"], + label: __('Assignment Based On'), + onchange: () => { + if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period") { + cur_dialog.set_df_property("effective_from", "read_only", 1); + cur_dialog.set_df_property("leave_period", "reqd", 1); + cur_dialog.set_df_property("effective_to", "read_only", 1); + } else { + cur_dialog.set_df_property("effective_from", "read_only", 0); + cur_dialog.set_df_property("leave_period", "reqd", 0); + cur_dialog.set_df_property("effective_to", "read_only", 0); + cur_dialog.set_value("effective_from", ""); + cur_dialog.set_value("effective_to", ""); + } + } + }, + { + fieldname: "leave_period", + fieldtype: 'Link', + options: "Leave Period", + label: __('Leave Period'), + depends_on: doc => { + return doc.assignment_based_on == 'Leave Period'; + }, + onchange: () => { + if (cur_dialog.fields_dict.leave_period.value) { + me.set_effective_date(); + } + } + }, + { + fieldtype: "Column Break" + }, + { + fieldname: 'effective_from', + fieldtype: 'Date', + label: __('Effective From'), + reqd: 1 + }, + { + fieldname: 'effective_to', + fieldtype: 'Date', + label: __('Effective To'), + reqd: 1 + }, + { + fieldname: 'carry_forward', + fieldtype: 'Check', + label: __('Add unused leaves from previous allocations') + } + ], + get_query() { + return { + filters: { + status: ['=', 'Active'] + } + }; + }, + add_filters_group: 1, + primary_action_label: "Assign", + action(employees, data) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees', + async: false, + args: { + employees: employees, + data: data + } + }); + cur_dialog.hide(); + } + }); + }); + + list_view.page.add_inner_button(__("Grant Leaves"), function () { + me.dialog = new frappe.ui.form.MultiSelectDialog({ + doctype: "Leave Policy Assignment", + target: cur_list, + setters: { + company: '', + employee: '', + }, + get_query() { + return { + filters: { + docstatus: ['=', 1], + leaves_allocated: ['=', 0] + } + }; + }, + add_filters_group: 1, + primary_action_label: "Grant Leaves", + action(leave_policy_assignments) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees', + async: false, + args: { + leave_policy_assignments: leave_policy_assignments + } + }); + me.dialog.hide(); + } + }); + }); + }, + + set_effective_date: function () { + if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period" && cur_dialog.fields_dict.leave_period.value) { + frappe.model.with_doc("Leave Period", cur_dialog.fields_dict.leave_period.value, function () { + let from_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "from_date"); + let to_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "to_date"); + cur_dialog.set_value("effective_from", from_date); + cur_dialog.set_value("effective_to", to_date); + }); + } + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py new file mode 100644 index 0000000000..c7bc6fb775 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.hr.doctype.leave_application.test_leave_application import get_leave_period, get_employee +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees +from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy + +class TestLeavePolicyAssignment(unittest.TestCase): + + def setUp(self): + for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + + def test_grant_leaves(self): + leave_period = get_leave_period() + employee = get_employee() + + # create the leave policy with leave type "_Test Leave Type", allocation = 10 + leave_policy = create_leave_policy() + leave_policy.submit() + + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) + leave_policy_assignment_doc.grant_leave_alloc_for_employee() + leave_policy_assignment_doc.reload() + + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + + leave_allocation = frappe.get_list("Leave Allocation", filters={ + "employee": employee.name, + "leave_policy":leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1})[0] + + leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) + + self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) + self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type") + self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date) + self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date) + self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name) + self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0]) + + def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self): + leave_period = get_leave_period() + employee = get_employee() + + # create the leave policy with leave type "_Test Leave Type", allocation = 10 + leave_policy = create_leave_policy() + leave_policy.submit() + + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) + leave_policy_assignment_doc.grant_leave_alloc_for_employee() + leave_policy_assignment_doc.reload() + + + # every leave is allocated no more leave can be granted now + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + + leave_allocation = frappe.get_list("Leave Allocation", filters={ + "employee": employee.name, + "leave_policy":leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1})[0] + + leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) + + # User all allowed to grant leave when there is no allocation against assignment + leave_alloc_doc.cancel() + leave_alloc_doc.delete() + + leave_policy_assignment_doc.reload() + + + # User are now allowed to grant leave + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + + def tearDown(self): + for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + + diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 4a135e0ffe..a2092919f8 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -33,6 +33,7 @@ "is_earned_leave", "earned_leave_frequency", "column_break_22", + "based_on_date_of_joining", "rounding" ], "fields": [ @@ -189,6 +190,13 @@ }, { "default": "0", + "depends_on": "eval:doc.is_earned_leave", + "description": "If checked, leave will be granted on the day of joining every month.", + "fieldname": "based_on_date_of_joining", + "fieldtype": "Check", + "label": "Based On Date Of Joining" + }, + { "depends_on": "eval:doc.is_lwp == 0", "fieldname": "is_ppl", "fieldtype": "Check", @@ -205,7 +213,7 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2020-08-26 14:04:54.318687", + "modified": "2020-10-15 15:49:47.555105", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 8d95924681..d700e7fccf 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -215,19 +215,6 @@ def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date): + _(") for {0}").format(exists_for) frappe.throw(msg) -def get_employee_leave_policy(employee): - leave_policy = frappe.db.get_value("Employee", employee, "leave_policy") - if not leave_policy: - employee_grade = frappe.db.get_value("Employee", employee, "grade") - if employee_grade: - leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy") - if not leave_policy: - frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade)) - if leave_policy: - return frappe.get_doc("Leave Policy", leave_policy) - else: - frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee)) - def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): existing_record = frappe.db.exists(doctype, { "payroll_period": payroll_period, @@ -300,43 +287,68 @@ def generate_leave_encashment(): def allocate_earned_leaves(): '''Allocate earned leaves to Employees''' - e_leave_types = frappe.get_all("Leave Type", - fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"], - filters={'is_earned_leave' : 1}) + e_leave_types = get_earned_leaves() today = getdate() - divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} for e_leave_type in e_leave_types: - leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s - between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1) + + leave_allocations = get_leave_allocations(today, e_leave_type.name) + for allocation in leave_allocations: - leave_policy = get_employee_leave_policy(allocation.employee) - if not leave_policy: + + if not allocation.leave_policy_assignment and not allocation.leave_policy: continue - if not e_leave_type.earned_leave_frequency == "Monthly": - if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency): - continue + + leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value( + "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]) + annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={ - 'parent': leave_policy.name, + 'parent': leave_policy, 'leave_type': e_leave_type.name }, fieldname=['annual_allocation']) - if annual_allocation: - earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency] - if e_leave_type.rounding == "0.5": - earned_leaves = round(earned_leaves * 2) / 2 - else: - earned_leaves = round(earned_leaves) - allocation = frappe.get_doc('Leave Allocation', allocation.name) - new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) + from_date=allocation.from_date - if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: - new_allocation = e_leave_type.max_leaves_allowed + if e_leave_type.based_on_date_of_joining_date: + from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if new_allocation == allocation.total_leaves_allocated: - continue - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_additional_leave_ledger_entry(allocation, earned_leaves, today) + if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): + update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) + +def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): + divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} + if annual_allocation: + earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency] + if e_leave_type.rounding == "0.5": + earned_leaves = round(earned_leaves * 2) / 2 + else: + earned_leaves = round(earned_leaves) + + allocation = frappe.get_doc('Leave Allocation', allocation.name) + new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) + + if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: + new_allocation = e_leave_type.max_leaves_allowed + + if new_allocation != allocation.total_leaves_allocated: + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + today_date = today() + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + + +def get_leave_allocations(date, leave_type): + return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy + from `tabLeave Allocation` + where + %s between from_date and to_date and docstatus=1 + and leave_type=%s""", + (date, leave_type), as_dict=1) + + +def get_earned_leaves(): + return frappe.get_all("Leave Type", + fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"], + filters={'is_earned_leave' : 1}) def create_additional_leave_ledger_entry(allocation, leaves, date): ''' Create leave ledger entry for leave types ''' @@ -345,24 +357,32 @@ def create_additional_leave_ledger_entry(allocation, leaves, date): allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() -def check_frequency_hit(from_date, to_date, frequency): - '''Return True if current date matches frequency''' - from_dt = get_datetime(from_date) - to_dt = get_datetime(to_date) +def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date): + import calendar from dateutil import relativedelta - rd = relativedelta.relativedelta(to_dt, from_dt) - months = rd.months - if frequency == "Quarterly": - if not months % 3: + + from_date = get_datetime(from_date) + to_date = get_datetime(to_date) + rd = relativedelta.relativedelta(to_date, from_date) + #last day of month + last_day = calendar.monthrange(to_date.year, to_date.month)[1] + + if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day): + if frequency == "Monthly": return True - elif frequency == "Half-Yearly": - if not months % 6: + elif frequency == "Quarterly" and rd.months % 3: return True - elif frequency == "Yearly": - if not months % 12: + elif frequency == "Half-Yearly" and rd.months % 6: return True + elif frequency == "Yearly" and rd.months % 12: + return True + + if frappe.flags.in_test: + return True + return False + def get_salary_assignment(employee, date): assignment = frappe.db.sql(""" select * from `tabSalary Structure Assignment` @@ -454,3 +474,10 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: total_claimed_amount = sum_of_claimed_amount[0].total_amount return total_claimed_amount + +def grant_leaves_automatically(): + automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy") + if automatically_allocate_leaves_based_on_leave_policy: + lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0}) + for assignment in lpa: + frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be884117..98b2fcdcab 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py new file mode 100644 index 0000000000..80c9137653 --- /dev/null +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -0,0 +1,77 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + if "leave_policy" in frappe.db.get_table_columns("Employee"): + employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1) + + employee_with_assignment = [] + leave_policy =[] + + #for employee + + for employee in employees_with_leave_policy: + alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) + if not alloc: + create_assignment(employee.name, employee.leave_policy) + + employee_with_assignment.append(employee.name) + leave_policy.append(employee.leave_policy) + + + if "default_leave_policy" in frappe.db.get_table_columns("Employee"): + employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1) + + #for whole employee Grade + + for grade in employee_grade_with_leave_policy: + employees = get_employee_with_grade(grade.name) + for employee in employees: + + if employee not in employee_with_assignment: #Will ensure no duplicate + alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}) + if not alloc: + create_assignment(employee.name, grade.default_leave_policy) + leave_policy.append(grade.default_leave_policy) + + #for old Leave allocation and leave policy from allocation, which may got updated in employee grade. + leave_allocations = frappe.db.sql("SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", as_dict = 1) + + for allocation in leave_allocations: + if allocation.leave_policy not in leave_policy: + create_assignment(allocation.employee, allocation.leave_policy, leave_period=allocation.leave_period, + allocation_exists=True) + +def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): + + filters = {"employee":employee, "leave_policy": leave_policy} + if leave_period: + filters["leave_period"] = leave_period + + if not frappe.db.exists("Leave Policy Assignment" , filters): + lpa = frappe.new_doc("Leave Policy Assignment") + lpa.employee = employee + lpa.leave_policy = leave_policy + + lpa.flags.ignore_mandatory = True + if allocation_exists: + lpa.assignment_based_on = 'Leave Period' + lpa.leave_period = leave_period + lpa.leaves_allocated = 1 + + lpa.save() + if allocation_exists: + lpa.submit() + #Updating old Leave Allocation + frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) + + +def get_employee_with_grade(grade): + return frappe.get_list("Employee", filters = {"grade": grade}) + + + From 31ac7d982a0a90ade9b82c0c8673e652e70224d0 Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 27 Nov 2020 15:58:07 +0530 Subject: [PATCH 206/229] chore(GitHub): Add issue template config --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..26bb7ab280 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Forum + url: https://discuss.erpnext.com/ + about: For general QnA, discussions and community help. From 38e4635a104e401f2a50c52a3c669c558abb0cc4 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sun, 29 Nov 2020 20:26:26 +0530 Subject: [PATCH 207/229] fix: import taxjar globally in the taxjar_integration module --- erpnext/erpnext_integrations/taxjar_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index 24fc3d44b9..f960998c3c 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -1,5 +1,7 @@ import traceback +import taxjar + import frappe from erpnext import get_default_company from frappe import _ @@ -29,7 +31,6 @@ def get_client(): def create_transaction(doc, method): - import taxjar """Create an order transaction in TaxJar""" if not TAXJAR_CREATE_TRANSACTIONS: From 2d5530da96237a2d746496efc7911b810f7e4047 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 29 Nov 2020 22:17:47 +0530 Subject: [PATCH 208/229] fix: Invoice discounting test --- .../invoice_discounting/invoice_discounting.py | 11 ++++++----- .../invoice_discounting/test_invoice_discounting.py | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 8083b21f75..af8940cde5 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -137,11 +137,12 @@ class InvoiceDiscounting(AccountsController): "cost_center": erpnext.get_default_cost_center(self.company) }) - je.append("accounts", { - "account": self.bank_charges_account, - "debit_in_account_currency": flt(self.bank_charges), - "cost_center": erpnext.get_default_cost_center(self.company) - }) + if self.bank_charges: + je.append("accounts", { + "account": self.bank_charges_account, + "debit_in_account_currency": flt(self.bank_charges), + "cost_center": erpnext.get_default_cost_center(self.company) + }) je.append("accounts", { "account": self.short_term_loan, diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py index 3d74d9a3b2..919dd0cba7 100644 --- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py @@ -80,6 +80,7 @@ class TestInvoiceDiscounting(unittest.TestCase): short_term_loan=self.short_term_loan, bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, + bank_charges=100 ) je = inv_disc.create_disbursement_entry() @@ -289,6 +290,7 @@ def create_invoice_discounting(invoices, **args): inv_disc.bank_account=args.bank_account inv_disc.loan_start_date = args.start or nowdate() inv_disc.loan_period = args.period or 30 + inv_disc.bank_charges = flt(args.bank_charges) for d in invoices: inv_disc.append("invoices", { From 66e8a12d0f010b9ec646964c56d59299d6fc842f Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 30 Nov 2020 17:48:13 +0530 Subject: [PATCH 209/229] fix: KeyError 'sourced_by_supplier' --- erpnext/manufacturing/doctype/bom/bom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8888a96768..c6699200dc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -169,8 +169,8 @@ class BOM(WebsiteGenerator): 'qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), - 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0, - 'sourced_by_supplier' : args['sourced_by_supplier'] or 0 + 'include_item_in_manufacturing': cint(args['transfer_for_manufacture'], 0), + 'sourced_by_supplier' : args.get('sourced_by_supplier', 0) } return ret_item From ccf5dc66e2162164210302f5a67bd67c8142a0af Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 1 Dec 2020 09:11:05 +0530 Subject: [PATCH 210/229] feat: multi-currency payroll (#23519) * feat: multi-currency payroll * fix: refactor and added conditions * fix: uncommented code * style: removed comments * fix: missing argument * style: styling changes * fix: test cases * Update asset_value_adjustment.py * patch: update columns * style: formating * style: formatting * fix: 1st review * fix: refactor * Revert "fix: refactor" This reverts commit eca0e17d11a192d60f249b2af992971c625aec46. reverting to previous state * Revert "fix: 1st review" This reverts commit 7eac48b102157df4353598f73b2ea97308af436a. reverting before 1st review * fix: 2nd review changes * fix: test cases * fix: added call to fetch exchange rate * fix: remove unnecessary code * fix: refactor * fix: refactor patch * fix: refactor * fix: refactor * fix: clear test data * fix: slider * feat: multi-currency payroll * fix: refactor and added conditions * fix: uncommented code * style: removed comments * fix: missing argument * style: styling changes * fix: test cases * patch: update columns * Update asset_value_adjustment.py * style: formating * style: formatting * fix: 1st review * fix: refactor * Revert "fix: refactor" This reverts commit eca0e17d11a192d60f249b2af992971c625aec46. reverting to previous state * Revert "fix: 1st review" This reverts commit 7eac48b102157df4353598f73b2ea97308af436a. reverting before 1st review * fix: 2nd review changes * fix: test cases * fix: added call to fetch exchange rate * fix: remove unnecessary code * fix: refactor * fix: refactor patch * fix: refactor * fix: refactor * fix: clear test data * fix: slider * feat: Added company field in leave encashment and employee benefit * refactor: Refactored multi-currency payroll patch * fix: currency column in salary register * refactor: Refactored code for making bank and return entry against employee advance * fix: minor cleanup * fix: fixed translation * fix: removed salary component type * fix: fixed sider issues * fix: translation and slider * style: formatted msg * fix: fixed slider * fix: travis * fix: refactor * fix: slider * fix: slider * fix: slider * fix: travis * fix: patch * fix: patch * fix: travis * fix: travis * fix: travis * fix: travis * fix: travis * fix: travis * fix: re-run travis * fix: rerun travis * fix: rerun travis * fix: rerun travis * fix: travis rerun * fix: increased throttle_user_limit from 60 to 100 * fix: patch * fix: patch * fix: assign payroll payable account as default payroll payable account in SSA * fix: removed debugger * fix: slider Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Co-authored-by: Nabin Hait --- .travis/site_config.json | 3 +- .../mode_of_payment/mode_of_payment.js | 24 +- .../doctype/payment_entry/payment_entry.py | 350 ++++++++++++------ .../salary_component_account.json | 118 ++---- erpnext/demo/setup/setup_data.py | 2 +- .../employee_advance/employee_advance.js | 89 ++++- .../employee_advance/employee_advance.json | 32 +- .../employee_advance/employee_advance.py | 104 ++++-- .../employee_advance/test_employee_advance.py | 12 +- .../expense_claim/test_expense_claim.py | 4 + .../expense_taxes_and_charges.json | 10 +- .../leave_encashment/leave_encashment.js | 24 +- .../leave_encashment/leave_encashment.json | 33 +- .../leave_encashment/leave_encashment.py | 7 + .../leave_encashment/test_leave_encashment.py | 10 +- .../loan_management/doctype/loan/loan.json | 2 +- erpnext/loan_management/doctype/loan/loan.py | 15 +- .../loan_management/doctype/loan/test_loan.py | 2 + .../loan_application/test_loan_application.py | 4 +- erpnext/patches.txt | 2 + .../create_salary_structure_assignments.py | 13 +- .../updates_for_multi_currency_payroll.py | 136 +++++++ .../additional_salary/additional_salary.js | 52 +++ .../additional_salary/additional_salary.json | 29 +- .../additional_salary/additional_salary.py | 5 + .../test_additional_salary.py | 15 +- .../employee_benefit_application.js | 50 ++- .../employee_benefit_application.json | 26 +- .../employee_benefit_application.py | 29 +- .../employee_benefit_application_detail.json | 4 +- .../employee_benefit_claim.js | 19 + .../employee_benefit_claim.json | 24 +- .../employee_incentive/employee_incentive.js | 53 ++- .../employee_incentive.json | 27 +- .../employee_incentive/employee_incentive.py | 9 + .../employee_tax_exemption_declaration.json | 14 +- ...test_employee_tax_exemption_declaration.py | 4 + ...ee_tax_exemption_declaration_category.json | 4 +- ...employee_tax_exemption_proof_submission.js | 4 + ...ployee_tax_exemption_proof_submission.json | 14 +- ...tax_exemption_proof_submission_detail.json | 6 +- .../income_tax_slab/income_tax_slab.js | 4 +- .../income_tax_slab/income_tax_slab.json | 16 +- .../income_tax_slab_other_charges.json | 6 +- .../payroll_employee_detail.json | 2 +- .../doctype/payroll_entry/payroll_entry.js | 40 ++ .../doctype/payroll_entry/payroll_entry.json | 32 +- .../doctype/payroll_entry/payroll_entry.py | 137 +++++-- .../payroll_entry/test_payroll_entry.py | 88 ++++- .../test_set_salary_components.js | 16 +- .../retention_bonus/retention_bonus.js | 17 + .../retention_bonus/retention_bonus.json | 18 +- .../salary_component/salary_component.js | 2 +- .../doctype/salary_detail/salary_detail.json | 9 +- .../doctype/salary_slip/salary_slip.js | 190 ++++++++-- .../doctype/salary_slip/salary_slip.json | 136 +++++-- .../doctype/salary_slip/salary_slip.py | 65 +++- .../doctype/salary_slip/test_salary_slip.py | 107 ++++-- .../salary_structure/salary_structure.js | 57 ++- .../salary_structure/salary_structure.json | 126 ++----- .../salary_structure/salary_structure.py | 57 ++- .../salary_structure/test_salary_structure.py | 26 +- .../salary_structure_assignment.js | 27 +- .../salary_structure_assignment.json | 28 +- .../salary_structure_assignment.py | 27 ++ .../taxable_salary_slab.json | 6 +- .../report/salary_register/salary_register.js | 24 +- .../report/salary_register/salary_register.py | 79 ++-- 68 files changed, 2043 insertions(+), 683 deletions(-) create mode 100644 erpnext/patches/v13_0/updates_for_multi_currency_payroll.py diff --git a/.travis/site_config.json b/.travis/site_config.json index dae80095d4..572bbd0853 100644 --- a/.travis/site_config.json +++ b/.travis/site_config.json @@ -9,5 +9,6 @@ "root_login": "root", "root_password": "travis", "host_name": "http://test_site:8000", - "install_apps": ["erpnext"] + "install_apps": ["erpnext"], + "throttle_user_limit": 100 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index d3040c8db8..7a06d3572a 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -1,13 +1,17 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - return{ - filters: [ - ['Account', 'account_type', 'in', 'Bank, Cash, Receivable'], - ['Account', 'is_group', '=', 0], - ['Account', 'company', '=', d.company] - ] - } -}); +frappe.ui.form.on('Mode of Payment', { + setup: function(frm) { + frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: [ + ['Account', 'account_type', 'in', 'Bank, Cash, Receivable'], + ['Account', 'is_group', '=', 0], + ['Account', 'company', '=', d.company] + ] + }; + }); + }, +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 11ab02021b..31a4c8a387 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -202,17 +202,32 @@ class PaymentEntry(AccountsController): # if account_type not in account_types: # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) - def set_exchange_rate(self): + def set_exchange_rate(self, ref_doc=None): + self.set_source_exchange_rate(ref_doc) + self.set_target_exchange_rate(ref_doc) + + def set_source_exchange_rate(self, ref_doc=None): if self.paid_from and not self.source_exchange_rate: if self.paid_from_account_currency == self.company_currency: self.source_exchange_rate = 1 else: - self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency, - self.company_currency, self.posting_date) + if ref_doc: + if self.paid_from_account_currency == ref_doc.currency: + self.source_exchange_rate = ref_doc.get("exchange_rate") + if not self.source_exchange_rate: + self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency, + self.company_currency, self.posting_date) + + def set_target_exchange_rate(self, ref_doc=None): if self.paid_to and not self.target_exchange_rate: - self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency, - self.company_currency, self.posting_date) + if ref_doc: + if self.paid_to_account_currency == ref_doc.currency: + self.target_exchange_rate = ref_doc.get("exchange_rate") + + if not self.target_exchange_rate: + self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency, + self.company_currency, self.posting_date) def validate_mandatory(self): for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): @@ -282,9 +297,10 @@ class PaymentEntry(AccountsController): no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) for k, v in no_oustanding_refs.items(): - frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.

\ - If this is undesirable please cancel the corresponding Payment Entry.") - .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")), + frappe.msgprint( + _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") + .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")) + + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -909,22 +925,24 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre exchange_rate = 1 outstanding_amount = get_outstanding_on_journal_entry(reference_name) elif reference_doctype != "Journal Entry": - if party_account_currency == company_currency: - if ref_doc.doctype == "Expense Claim": + if ref_doc.doctype == "Expense Claim": total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) - elif ref_doc.doctype == "Employee Advance": - total_amount = ref_doc.advance_amount - else: + elif ref_doc.doctype == "Employee Advance": + total_amount = ref_doc.advance_amount + exchange_rate = ref_doc.get("exchange_rate") + if party_account_currency != ref_doc.currency: + total_amount = flt(total_amount) * flt(exchange_rate) + if not total_amount: + if party_account_currency == company_currency: total_amount = ref_doc.base_grand_total - exchange_rate = 1 - else: - total_amount = ref_doc.grand_total - + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total + if not exchange_rate: # Get the exchange rate from the original ref doc - # or get it based on the posting date of the ref doc + # or get it based on the posting date of the ref doc. exchange_rate = ref_doc.get("conversion_rate") or \ get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) - if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") @@ -932,11 +950,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) elif reference_doctype == "Employee Advance": - outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount) + outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)) + if party_account_currency != ref_doc.currency: + outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) + if party_account_currency == company_currency: + exchange_rate = 1 else: outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) else: - # Get the exchange rate based on the posting date of the ref doc + # Get the exchange rate based on the posting date of the ref doc. exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) @@ -948,102 +970,104 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre "bill_no": bill_no }) +def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name): + total_amount, outstanding_amount, exchange_rate = None + if reference_doctype == "Fees": + total_amount = ref_doc.get("grand_total") + exchange_rate = 1 + outstanding_amount = ref_doc.get("outstanding_amount") + elif reference_doctype == "Dunning": + total_amount = ref_doc.get("dunning_amount") + exchange_rate = 1 + outstanding_amount = ref_doc.get("dunning_amount") + elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + total_amount = ref_doc.get("total_amount") + if ref_doc.multi_currency: + exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + else: + exchange_rate = 1 + outstanding_amount = get_outstanding_on_journal_entry(reference_name) + + return total_amount, outstanding_amount, exchange_rate + +def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency): + total_amount, outstanding_amount, exchange_rate = None + if ref_doc.doctype == "Expense Claim": + total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) + elif ref_doc.doctype == "Employee Advance": + total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) + + if not total_amount: + total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( + party_account_currency, company_currency, ref_doc) + + if not exchange_rate: + # Get the exchange rate from the original ref doc + # or get it based on the posting date of the ref doc + exchange_rate = ref_doc.get("conversion_rate") or \ + get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + + outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts( + reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency) + + return total_amount, outstanding_amount, exchange_rate, bill_no + +def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc): + total_amount = ref_doc.advance_amount + exchange_rate = ref_doc.get("exchange_rate") + if party_account_currency != ref_doc.currency: + total_amount = flt(total_amount) * flt(exchange_rate) + + return total_amount, exchange_rate + +def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc): + exchange_rate = None + if party_account_currency == company_currency: + total_amount = ref_doc.base_grand_total + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total + + return total_amount, exchange_rate + +def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency): + outstanding_amount, bill_no = None + if reference_doctype in ("Sales Invoice", "Purchase Invoice"): + outstanding_amount = ref_doc.get("outstanding_amount") + bill_no = ref_doc.get("bill_no") + elif reference_doctype == "Expense Claim": + outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ + - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) + elif reference_doctype == "Employee Advance": + outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)) + if party_account_currency != ref_doc.currency: + outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) + if party_account_currency == company_currency: + exchange_rate = 1 + else: + outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) + + return outstanding_amount, exchange_rate, bill_no + @frappe.whitelist() def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): + reference_doc = None doc = frappe.get_doc(dt, dn) if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) - if dt in ("Sales Invoice", "Sales Order", "Dunning"): - party_type = "Customer" - elif dt in ("Purchase Invoice", "Purchase Order"): - party_type = "Supplier" - elif dt in ("Expense Claim", "Employee Advance"): - party_type = "Employee" - elif dt in ("Fees"): - party_type = "Student" - - # party account - if dt == "Sales Invoice": - party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to - elif dt == "Purchase Invoice": - party_account = doc.credit_to - elif dt == "Fees": - party_account = doc.receivable_account - elif dt == "Employee Advance": - party_account = doc.advance_account - elif dt == "Expense Claim": - party_account = doc.payable_account - else: - party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) - - if dt not in ("Sales Invoice", "Purchase Invoice"): - party_account_currency = get_account_currency(party_account) - else: - party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) - - # payment type - if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ - or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): - payment_type = "Receive" - else: - payment_type = "Pay" - - # amounts - grand_total = outstanding_amount = 0 - if party_amount: - grand_total = outstanding_amount = party_amount - elif dt in ("Sales Invoice", "Purchase Invoice"): - if party_account_currency == doc.company_currency: - grand_total = doc.base_rounded_total or doc.base_grand_total - else: - grand_total = doc.rounded_total or doc.grand_total - outstanding_amount = doc.outstanding_amount - elif dt in ("Expense Claim"): - grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges - outstanding_amount = doc.grand_total \ - - doc.total_amount_reimbursed - elif dt == "Employee Advance": - grand_total = doc.advance_amount - outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) - elif dt == "Fees": - grand_total = doc.grand_total - outstanding_amount = doc.outstanding_amount - elif dt == "Dunning": - grand_total = doc.grand_total - outstanding_amount = doc.grand_total - else: - if party_account_currency == doc.company_currency: - grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) - else: - grand_total = flt(doc.get("rounded_total") or doc.grand_total) - outstanding_amount = grand_total - flt(doc.advance_paid) + party_type = set_party_type(dt) + party_account = set_party_account(dt, dn, doc, party_type) + party_account_currency = set_party_account_currency(dt, party_account, doc) + payment_type = set_payment_type(dt, doc) + grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc) # bank or cash - bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), - account=bank_account) + bank = get_bank_cash_account(doc, bank_account) - if not bank: - bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), - account=bank_account) - - paid_amount = received_amount = 0 - if party_account_currency == bank.account_currency: - paid_amount = received_amount = abs(outstanding_amount) - elif payment_type == "Receive": - paid_amount = abs(outstanding_amount) - if bank_amount: - received_amount = bank_amount - else: - received_amount = paid_amount * doc.get('conversion_rate', 1) - else: - received_amount = abs(outstanding_amount) - if bank_amount: - paid_amount = bank_amount - else: - # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get('conversion_rate', 1) + paid_amount, received_amount = set_paid_amount_and_received_amount( + dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc) pe = frappe.new_doc("Payment Entry") pe.payment_type = payment_type @@ -1115,10 +1139,120 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.setup_party_account_field() pe.set_missing_values() if party_account and bank: - pe.set_exchange_rate() + if dt == "Employee Advance": + reference_doc = doc + pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() return pe +def get_bank_cash_account(doc, bank_account): + bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), + account=bank_account) + + if not bank: + bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), + account=bank_account) + + return bank + +def set_party_type(dt): + if dt in ("Sales Invoice", "Sales Order", "Dunning"): + party_type = "Customer" + elif dt in ("Purchase Invoice", "Purchase Order"): + party_type = "Supplier" + elif dt in ("Expense Claim", "Employee Advance"): + party_type = "Employee" + elif dt in ("Fees"): + party_type = "Student" + return party_type + +def set_party_account(dt, dn, doc, party_type): + if dt == "Sales Invoice": + party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to + elif dt == "Purchase Invoice": + party_account = doc.credit_to + elif dt == "Fees": + party_account = doc.receivable_account + elif dt == "Employee Advance": + party_account = doc.advance_account + elif dt == "Expense Claim": + party_account = doc.payable_account + else: + party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) + return party_account + +def set_party_account_currency(dt, party_account, doc): + if dt not in ("Sales Invoice", "Purchase Invoice"): + party_account_currency = get_account_currency(party_account) + else: + party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) + return party_account_currency + +def set_payment_type(dt, doc): + if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ + or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): + payment_type = "Receive" + else: + payment_type = "Pay" + return payment_type + +def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc): + grand_total = outstanding_amount = 0 + if party_amount: + grand_total = outstanding_amount = party_amount + elif dt in ("Sales Invoice", "Purchase Invoice"): + if party_account_currency == doc.company_currency: + grand_total = doc.base_rounded_total or doc.base_grand_total + else: + grand_total = doc.rounded_total or doc.grand_total + outstanding_amount = doc.outstanding_amount + elif dt in ("Expense Claim"): + grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges + outstanding_amount = doc.grand_total \ + - doc.total_amount_reimbursed + elif dt == "Employee Advance": + grand_total = flt(doc.advance_amount) + outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) + if party_account_currency != doc.currency: + grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate) + outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate) + elif dt == "Fees": + grand_total = doc.grand_total + outstanding_amount = doc.outstanding_amount + elif dt == "Dunning": + grand_total = doc.grand_total + outstanding_amount = doc.grand_total + else: + if party_account_currency == doc.company_currency: + grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) + else: + grand_total = flt(doc.get("rounded_total") or doc.grand_total) + outstanding_amount = grand_total - flt(doc.advance_paid) + return grand_total, outstanding_amount + +def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc): + paid_amount = received_amount = 0 + if party_account_currency == bank.account_currency: + paid_amount = received_amount = abs(outstanding_amount) + elif payment_type == "Receive": + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount + else: + received_amount = paid_amount * doc.get('conversion_rate', 1) + if dt == "Employee Advance": + received_amount = paid_amount * doc.get('exchange_rate', 1) + else: + received_amount = abs(outstanding_amount) + if bank_amount: + paid_amount = bank_amount + else: + # if party account currency and bank currency is different then populate paid amount as well + paid_amount = received_amount * doc.get('conversion_rate', 1) + if dt == "Employee Advance": + paid_amount = received_amount * doc.get('exchange_rate', 1) + return paid_amount, received_amount + def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): references = [] for payment_term in payment_schedule: diff --git a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json index 23dc6c47e8..f1ed8efa31 100644 --- a/erpnext/accounts/doctype/salary_component_account/salary_component_account.json +++ b/erpnext/accounts/doctype/salary_component_account/salary_component_account.json @@ -1,92 +1,38 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-07-27 17:24:24.956896", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-07-27 17:24:24.956896", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "account" + ], "fields": [ { - "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": 0, - "in_list_view": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.", - "fieldname": "default_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Default Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.", + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-09-02 07:49:06.567389", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Salary Component Account", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-10-18 17:57:57.110257", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Salary Component Account", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/demo/setup/setup_data.py b/erpnext/demo/setup/setup_data.py index a395c7c17a..05ee28a24a 100644 --- a/erpnext/demo/setup/setup_data.py +++ b/erpnext/demo/setup/setup_data.py @@ -134,7 +134,7 @@ def setup_employee(): salary_component = frappe.get_doc('Salary Component', d.name) salary_component.append('accounts', dict( company=erpnext.get_default_company(), - default_account=frappe.get_value('Account', dict(account_name=('like', 'Salary%'))) + account=frappe.get_value('Account', dict(account_name=('like', 'Salary%'))) )) salary_component.save() diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index cba8ee9a40..7056adf208 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -15,11 +15,16 @@ frappe.ui.form.on('Employee Advance', { }); frm.set_query("advance_account", function() { + if (!frm.doc.employee) { + frappe.msgprint(__("Please select employee first")); + } + var company_currency = erpnext.get_currency(frm.doc.company); return { filters: { "root_type": "Asset", "is_group": 0, - "company": frm.doc.company + "company": frm.doc.company, + "account_currency": ["in", [frm.doc.currency, company_currency]], } }; }); @@ -63,7 +68,7 @@ frappe.ui.form.on('Employee Advance', { }, __('Create')); }else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){ frm.add_custom_button(__("Deduction from salary"), function() { - frm.events.make_deduction_via_additional_salary(frm) + frm.events.make_deduction_via_additional_salary(frm); }, __('Create')); } } @@ -127,7 +132,9 @@ frappe.ui.form.on('Employee Advance', { 'employee_advance_name': frm.doc.name, 'return_amount': flt(frm.doc.paid_amount - frm.doc.claimed_amount), 'advance_account': frm.doc.advance_account, - 'mode_of_payment': frm.doc.mode_of_payment + 'mode_of_payment': frm.doc.mode_of_payment, + 'currency': frm.doc.currency, + 'exchange_rate': frm.doc.exchange_rate }, callback: function(r) { const doclist = frappe.model.sync(r.message); @@ -138,16 +145,72 @@ frappe.ui.form.on('Employee Advance', { employee: function (frm) { if (frm.doc.employee) { - return frappe.call({ - method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount", - args: { - "employee": frm.doc.employee, - "posting_date": frm.doc.posting_date - }, - callback: function(r) { - frm.set_value("pending_amount",r.message); - } - }); + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('get_pending_amount') + ]); } + }, + + get_pending_amount: function(frm) { + frappe.call({ + method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount", + args: { + "employee": frm.doc.employee, + "posting_date": frm.doc.posting_date + }, + callback: function(r) { + frm.set_value("pending_amount", r.message); + } + }); + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, + + currency: function(frm) { + var from_currency = frm.doc.currency; + var company_currency; + if (!frm.doc.company) { + company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); + } else { + company_currency = erpnext.get_currency(frm.doc.company); + } + if (from_currency != company_currency) { + frm.events.set_exchange_rate(frm, from_currency, company_currency); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + frm.refresh_fields(); + }, + + set_exchange_rate: function(frm, from_currency, company_currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: from_currency, + to_currency: company_currency, + }, + callback: function(r) { + frm.set_value("exchange_rate", flt(r.message)); + frm.set_df_property('exchange_rate', 'hidden', 0); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); + } + }); } }); diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 0d90913871..cf6b5404ec 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -13,6 +13,8 @@ "department", "column_break_4", "posting_date", + "currency", + "exchange_rate", "repay_unclaimed_amount_from_salary", "section_break_8", "purpose", @@ -91,7 +93,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Advance Amount", - "options": "Company:company:default_currency", + "options": "currency", "reqd": 1 }, { @@ -99,7 +101,7 @@ "fieldtype": "Currency", "label": "Paid Amount", "no_copy": 1, - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -107,7 +109,7 @@ "fieldtype": "Currency", "label": "Claimed Amount", "no_copy": 1, - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -161,7 +163,7 @@ "fieldname": "return_amount", "fieldtype": "Currency", "label": "Returned Amount", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -175,13 +177,31 @@ "fieldname": "pending_amount", "fieldtype": "Currency", "label": "Pending Amount", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "depends_on": "currency", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-12 12:42:39.833818", + "modified": "2020-11-25 12:01:55.980721", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 3c435b8cc3..cb72f6b6d9 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -19,7 +19,6 @@ class EmployeeAdvance(Document): def validate(self): self.set_status() - self.validate_employee_advance_account() def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') @@ -38,16 +37,9 @@ class EmployeeAdvance(Document): elif self.docstatus == 2: self.status = "Cancelled" - def validate_employee_advance_account(self): - company_currency = erpnext.get_company_currency(self.company) - if (self.advance_account and - company_currency != frappe.db.get_value('Account', self.advance_account, 'account_currency')): - frappe.throw(_("Advance account currency should be same as company currency {0}") - .format(company_currency)) - def set_total_advance_paid(self): paid_amount = frappe.db.sql(""" - select ifnull(sum(debit_in_account_currency), 0) as paid_amount + select ifnull(sum(debit), 0) as paid_amount from `tabGL Entry` where against_voucher_type = 'Employee Advance' and against_voucher = %s @@ -56,7 +48,7 @@ class EmployeeAdvance(Document): """, (self.name, self.employee), as_dict=1)[0].paid_amount return_amount = frappe.db.sql(""" - select name, ifnull(sum(credit_in_account_currency), 0) as return_amount + select ifnull(sum(credit), 0) as return_amount from `tabGL Entry` where against_voucher_type = 'Employee Advance' and voucher_type != 'Expense Claim' @@ -65,6 +57,11 @@ class EmployeeAdvance(Document): and party = %s """, (self.name, self.employee), as_dict=1)[0].return_amount + if paid_amount != 0: + paid_amount = flt(paid_amount) / flt(self.exchange_rate) + if return_amount != 0: + return_amount = flt(return_amount) / flt(self.exchange_rate) + if flt(paid_amount) > self.advance_amount: frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"), EmployeeAdvanceOverPayment) @@ -107,16 +104,27 @@ def make_bank_entry(dt, dn): doc = frappe.get_doc(dt, dn) payment_account = get_default_bank_cash_account(doc.company, account_type="Cash", mode_of_payment=doc.mode_of_payment) + if not payment_account: + frappe.throw(_("Please set a Default Cash Account in Company defaults")) + + advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency') + + advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc ) + + paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc) je = frappe.new_doc("Journal Entry") je.posting_date = nowdate() je.voucher_type = 'Bank Entry' je.company = doc.company je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose + je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0 je.append("accounts", { "account": doc.advance_account, - "debit_in_account_currency": flt(doc.advance_amount), + "account_currency": advance_account_currency, + "exchange_rate": flt(advance_exchange_rate), + "debit_in_account_currency": flt(advance_amount), "reference_type": "Employee Advance", "reference_name": doc.name, "party_type": "Employee", @@ -128,19 +136,41 @@ def make_bank_entry(dt, dn): je.append("accounts", { "account": payment_account.account, "cost_center": erpnext.get_default_cost_center(doc.company), - "credit_in_account_currency": flt(doc.advance_amount), + "credit_in_account_currency": flt(paying_amount), "account_currency": payment_account.account_currency, - "account_type": payment_account.account_type + "account_type": payment_account.account_type, + "exchange_rate": flt(paying_exchange_rate) }) return je.as_dict() +def get_advance_amount_advance_exchange_rate(advance_account_currency, doc): + if advance_account_currency != doc.currency: + advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) + advance_exchange_rate = 1 + else: + advance_amount = doc.advance_amount + advance_exchange_rate = doc.exchange_rate + + return advance_amount, advance_exchange_rate + +def get_paying_amount_paying_exchange_rate(payment_account, doc): + if payment_account.account_currency != doc.currency: + paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) + paying_exchange_rate = 1 + else: + paying_amount = doc.advance_amount + paying_exchange_rate = doc.exchange_rate + + return paying_amount, paying_exchange_rate + @frappe.whitelist() def create_return_through_additional_salary(doc): import json doc = frappe._dict(json.loads(doc)) additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = doc.employee + additional_salary.currency = doc.currency additional_salary.amount = doc.paid_amount - doc.claimed_amount additional_salary.company = doc.company additional_salary.ref_doctype = doc.doctype @@ -149,26 +179,28 @@ def create_return_through_additional_salary(doc): return additional_salary @frappe.whitelist() -def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None): - return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) - - mode_of_payment_type = '' - if mode_of_payment: - mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') - if mode_of_payment_type not in ["Cash", "Bank"]: - # if mode of payment is General then it unset the type - mode_of_payment_type = None - +def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None): + bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) + if not bank_cash_account: + frappe.throw(_("Please set a Default Cash Account in Company defaults")) + + advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') + je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() - # if mode of payment is Bank then voucher type is Bank Entry - je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry' + je.voucher_type = get_voucher_type(mode_of_payment) je.company = company je.remark = 'Return against Employee Advance: ' + employee_advance_name + je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0 + + advance_account_amount = flt(return_amount) if advance_account_currency==currency \ + else flt(return_amount) * flt(exchange_rate) je.append('accounts', { 'account': advance_account, - 'credit_in_account_currency': return_amount, + 'credit_in_account_currency': advance_account_amount, + 'account_currency': advance_account_currency, + 'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1, 'reference_type': 'Employee Advance', 'reference_name': employee_advance_name, 'party_type': 'Employee', @@ -176,13 +208,25 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, 'is_advance': 'Yes' }) + bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ + else flt(return_amount) * flt(exchange_rate) + je.append("accounts", { - "account": return_account.account, - "debit_in_account_currency": return_amount, - "account_currency": return_account.account_currency, - "account_type": return_account.account_type + "account": bank_cash_account.account, + "debit_in_account_currency": bank_amount, + "account_currency": bank_cash_account.account_currency, + "account_type": bank_cash_account.account_type, + "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 }) return je.as_dict() +def get_voucher_type(mode_of_payment=None): + voucher_type = "Cash Entry" + if mode_of_payment: + mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') + if mode_of_payment_type == "Bank": + voucher_type = "Bank Entry" + + return voucher_type \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 2097e711de..c88b2b8e49 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -3,15 +3,17 @@ # See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext import unittest from frappe.utils import nowdate from erpnext.hr.doctype.employee_advance.employee_advance import make_bank_entry from erpnext.hr.doctype.employee_advance.employee_advance import EmployeeAdvanceOverPayment +from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeAdvance(unittest.TestCase): def test_paid_amount_and_status(self): - advance = make_employee_advance() + employee_name = make_employee("_T@employe.advance") + advance = make_employee_advance(employee_name) journal_entry = make_payment_entry(advance) journal_entry.submit() @@ -33,11 +35,13 @@ def make_payment_entry(advance): return journal_entry -def make_employee_advance(): +def make_employee_advance(employee_name): doc = frappe.new_doc("Employee Advance") - doc.employee = "_T-Employee-00001" + doc.employee = employee_name doc.company = "_Test company" doc.purpose = "For site visit" + doc.currency = erpnext.get_company_currency("_Test company") + doc.exchange_rate = 1 doc.advance_amount = 1000 doc.posting_date = nowdate() doc.advance_account = "_Test Employee Advance - _TC" diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 6e97f0513d..4a0908d457 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -7,6 +7,7 @@ import unittest from frappe.utils import random_string, nowdate from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.hr.doctype.employee.test_employee import make_employee test_records = frappe.get_test_records('Expense Claim') test_dependencies = ['Employee'] @@ -126,6 +127,9 @@ 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"}) + if not employee: + employee = make_employee("test_employee@expense_claim.com", company=company) + currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) expense_claim = { "doctype": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json index 885e3eed97..020457d4ec 100644 --- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -71,9 +71,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "oldfieldname": "tax_amount", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency" + "options": "currency" }, { "columns": 2, @@ -81,9 +79,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Total", - "oldfieldname": "total", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -106,7 +102,7 @@ ], "istable": 1, "links": [], - "modified": "2020-05-11 19:01:26.611758", + "modified": "2020-09-23 20:27:36.027728", "modified_by": "Administrator", "module": "HR", "name": "Expense Taxes and Charges", diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.js b/erpnext/hr/doctype/leave_encashment/leave_encashment.js index 71a34226da..81936a4a38 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.js +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.js @@ -22,7 +22,12 @@ frappe.ui.form.on('Leave Encashment', { } }, employee: function(frm) { - frm.trigger("get_leave_details_for_encashment"); + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('get_leave_details_for_encashment') + ]); + } }, leave_type: function(frm) { frm.trigger("get_leave_details_for_encashment"); @@ -40,5 +45,20 @@ frappe.ui.form.on('Leave Encashment', { } }); } - } + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, }); diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json index 2cf6ccf5ca..83eeae3adb 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json @@ -12,6 +12,7 @@ "employee", "employee_name", "department", + "company", "column_break_4", "leave_type", "leave_allocation", @@ -19,9 +20,11 @@ "encashable_days", "amended_from", "payroll", - "encashment_amount", "encashment_date", - "additional_salary" + "additional_salary", + "column_break_14", + "currency", + "encashment_amount" ], "fields": [ { @@ -109,6 +112,7 @@ "in_list_view": 1, "label": "Encashment Amount", "no_copy": 1, + "options": "currency", "read_only": 1 }, { @@ -124,11 +128,34 @@ "no_copy": 1, "options": "Additional Salary", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2019-12-16 11:51:57.732223", + "modified": "2020-11-25 11:56:06.777241", "modified_by": "Administrator", "module": "HR", "name": "Leave Encashment", diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index c1dcc97b1a..4c1a46522f 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -16,10 +16,16 @@ class LeaveEncashment(Document): def validate(self): set_employee_name(self) self.get_leave_details_for_encashment() + self.validate_salary_structure() if not self.encashment_date: self.encashment_date = getdate(nowdate()) + def validate_salary_structure(self): + if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + + def before_submit(self): if self.encashment_amount <= 0: frappe.throw(_("You can only submit Leave Encashment for a valid encashment amount")) @@ -30,6 +36,7 @@ class LeaveEncashment(Document): additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee + additional_salary.currency = self.currency earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") if not earning_component: frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type)) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index bbee18bb0a..aafc9642d4 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -48,6 +48,10 @@ class TestLeaveEncashment(unittest.TestCase): frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() + def tearDown(self): + for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]: + frappe.db.sql("delete from `tab%s`" % dt) + def test_leave_balance_value_and_amount(self): frappe.db.sql('''delete from `tabLeave Encashment`''') leave_encashment = frappe.get_doc(dict( @@ -55,7 +59,8 @@ class TestLeaveEncashment(unittest.TestCase): employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today() + payroll_date=today(), + currency="INR" )).insert() self.assertEqual(leave_encashment.leave_balance, 10) @@ -75,7 +80,8 @@ class TestLeaveEncashment(unittest.TestCase): employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today() + payroll_date=today(), + currency="INR" )).insert() leave_encashment.submit() diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index d468f52bc0..acf09f5c03 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -26,11 +26,11 @@ "disbursed_amount", "column_break_11", "maximum_loan_amount", - "is_term_loan", "repayment_method", "repayment_periods", "monthly_repayment_amount", "repayment_start_date", + "is_term_loan", "account_info", "mode_of_payment", "payment_account", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 8405d6ec62..cd40a665d4 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -13,6 +13,8 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calcul class Loan(AccountsController): def validate(self): + if self.applicant_type == 'Employee' and self.repay_from_salary: + validate_employee_currency_with_company_currency(self.applicant, self.company) self.set_loan_amount() self.validate_loan_amount() self.set_missing_fields() @@ -329,5 +331,14 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a return unpledge_request - - +def validate_employee_currency_with_company_currency(applicant, company): + from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency + if not applicant: + frappe.throw(_("Please select Applicant")) + if not company: + frappe.throw(_("Please select Company")) + employee_currency = get_employee_currency(applicant) + company_currency = erpnext.get_company_currency(company) + if employee_currency != company_currency: + frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") + .format(applicant, employee_currency)) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 10a7b1143d..a63d06590f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -19,6 +19,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestLoan(unittest.TestCase): def setUp(self): @@ -44,6 +45,7 @@ class TestLoan(unittest.TestCase): create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) self.applicant1 = make_employee("robert_loan@loan.com") + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index 687c58000e..2a659e9fc2 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee, make_salary_structure from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts class TestLoanApplication(unittest.TestCase): @@ -14,6 +14,7 @@ class TestLoanApplication(unittest.TestCase): create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) self.applicant = make_employee("kate_loan@loan.com", "_Test Company") + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') self.create_loan_application() def create_loan_application(self): @@ -29,7 +30,6 @@ class TestLoanApplication(unittest.TestCase): }) loan_application.insert() - def test_loan_totals(self): loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant}) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 98b2fcdcab..61aa2eec59 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -732,7 +732,9 @@ erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail +erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index c51c38182c..a908c16715 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -8,8 +8,8 @@ from frappe.utils import getdate from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import DuplicateAssignment def execute(): - frappe.reload_doc('Payroll', 'doctype', 'salary_structure') - frappe.reload_doc("Payroll", "doctype", "salary_structure_assignment") + frappe.reload_doc('Payroll', 'doctype', 'Salary Structure') + frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment") frappe.db.sql(""" delete from `tabSalary Structure Assignment` where salary_structure in (select name from `tabSalary Structure` where is_active='No' or docstatus!=1) @@ -33,6 +33,13 @@ def execute(): AND employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left') """.format(cols), as_dict=1) + all_companies = frappe.db.get_all("Company", fields=["name", "default_currency"]) + for d in all_companies: + company = d.name + company_currency = d.default_currency + + frappe.db.sql("""update `tabSalary Structure` set currency = %s where company=%s""", (company_currency, company)) + for d in ss_details: try: joining_date, relieving_date = frappe.db.get_value("Employee", d.employee, @@ -42,6 +49,7 @@ def execute(): from_date = joining_date elif relieving_date and getdate(from_date) > relieving_date: continue + company_currency = frappe.db.get_value('Company', d.company, 'default_currency') s = frappe.new_doc("Salary Structure Assignment") s.employee = d.employee @@ -52,6 +60,7 @@ def execute(): s.base = d.get("base") s.variable = d.get("variable") s.company = d.company + s.currency = company_currency # to migrate the data of the old employees s.flags.old_employee = True diff --git a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py new file mode 100644 index 0000000000..340bf4947b --- /dev/null +++ b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py @@ -0,0 +1,136 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.model.utils.rename_field import rename_field + +def execute(): + + frappe.reload_doc('Accounts', 'doctype', 'Salary Component Account') + if frappe.db.has_column('Salary Component Account', 'default_account'): + rename_field("Salary Component Account", "default_account", "account") + + doctype_list = [ + { + 'module':'HR', + 'doctype':'Employee Advance' + }, + { + 'module':'HR', + 'doctype':'Leave Encashment' + }, + { + 'module':'Payroll', + 'doctype':'Additional Salary' + }, + { + 'module':'Payroll', + 'doctype':'Employee Benefit Application' + }, + { + 'module':'Payroll', + 'doctype':'Employee Benefit Claim' + }, + { + 'module':'Payroll', + 'doctype':'Employee Incentive' + }, + { + 'module':'Payroll', + 'doctype':'Employee Tax Exemption Declaration' + }, + { + 'module':'Payroll', + 'doctype':'Employee Tax Exemption Proof Submission' + }, + { + 'module':'Payroll', + 'doctype':'Income Tax Slab' + }, + { + 'module':'Payroll', + 'doctype':'Payroll Entry' + }, + { + 'module':'Payroll', + 'doctype':'Retention Bonus' + }, + { + 'module':'Payroll', + 'doctype':'Salary Structure' + }, + { + 'module':'Payroll', + 'doctype':'Salary Structure Assignment' + }, + { + 'module':'Payroll', + 'doctype':'Salary Slip' + }, + ] + + for item in doctype_list: + frappe.reload_doc(item['module'], 'doctype', item['doctype']) + + # update company in employee advance based on employee company + for dt in ['Employee Incentive', 'Leave Encashment', 'Employee Benefit Application', 'Employee Benefit Claim']: + frappe.db.sql(""" + update `tab{doctype}` + set company = (select company from tabEmployee where name=`tab{doctype}`.employee) + """.format(doctype=dt)) + + # update exchange rate for employee advance + frappe.db.sql("update `tabEmployee Advance` set exchange_rate=1") + + # get all companies and it's currency + all_companies = frappe.db.get_all("Company", fields=["name", "default_currency", "default_payroll_payable_account"]) + for d in all_companies: + company = d.name + company_currency = d.default_currency + default_payroll_payable_account = d.default_payroll_payable_account + + if not default_payroll_payable_account: + default_payroll_payable_account = frappe.db.get_value("Account", + {"account_name": _("Payroll Payable"), "company": company, "account_currency": company_currency, "is_group": 0}) + + # update currency in following doctypes based on company currency + doctypes_for_currency = ['Employee Advance', 'Leave Encashment', 'Employee Benefit Application', + 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', + 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', + 'Income Tax Slab', 'Retention Bonus', 'Salary Structure'] + + for dt in doctypes_for_currency: + frappe.db.sql("""update `tab{doctype}` set currency = %s where company=%s""" + .format(doctype=dt), (company_currency, company)) + + # update fields in payroll entry + frappe.db.sql(""" + update `tabPayroll Entry` + set currency = %s, + exchange_rate = 1, + payroll_payable_account=%s + where company=%s + """, (company_currency, default_payroll_payable_account, company)) + + # update fields in Salary Structure Assignment + frappe.db.sql(""" + update `tabSalary Structure Assignment` + set currency = %s, + payroll_payable_account=%s + where company=%s + """, (company_currency, default_payroll_payable_account, company)) + + # update fields in Salary Slip + frappe.db.sql(""" + update `tabSalary Slip` + set currency = %s, + exchange_rate = 1, + base_hour_rate = hour_rate, + base_gross_pay = gross_pay, + base_total_deduction = total_deduction, + base_net_pay = net_pay, + base_rounded_total = rounded_total, + base_total_in_words = total_in_words + where company=%s + """, (company_currency, company)) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d56cd4e967..0784de93eb 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,5 +12,57 @@ frappe.ui.form.on('Additional Salary', { } }; }); + + if (!frm.doc.currency) return; + frm.set_query("salary_component", function() { + return { + query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {currency: frm.doc.currency, company: frm.doc.company} + }; + }); + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_company') + ]); + } else { + frm.set_value("company", null); + } + }, + + set_company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Employee", + fieldname: "company", + filters: { + name: frm.doc.employee + } + }, + callback: function(data) { + if (data.message) { + frm.set_value("company", data.message.company); + } + } + }); + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); }, }); diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json index 69cb5da893..2b29f667fb 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.json +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json @@ -11,20 +11,21 @@ "employee", "employee_name", "salary_component", - "overwrite_salary_structure_amount", - "deduct_full_tax_on_selected_payroll_date", + "type", + "amount", "ref_doctype", "ref_docname", + "amended_from", "column_break_5", "company", - "is_recurring", + "department", + "currency", "from_date", "to_date", "payroll_date", - "type", - "department", - "amount", - "amended_from" + "is_recurring", + "overwrite_salary_structure_amount", + "deduct_full_tax_on_selected_payroll_date" ], "fields": [ { @@ -59,6 +60,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", + "options": "currency", "reqd": 1 }, { @@ -159,11 +161,22 @@ "label": "Reference Document", "options": "ref_doctype", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 21:10:50.374063", + "modified": "2020-10-20 17:51:13.419716", "modified_by": "Administrator", "module": "Payroll", "name": "Additional Salary", diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index e3dc9070ec..f5af677fce 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -22,10 +22,15 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_salary_structure() self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + def validate_salary_structure(self): + if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + def validate_recurring_additional_salary_overlap(self): if self.is_recurring: additional_salaries = frappe.db.sql(""" diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index de26543b57..4d47f25fcf 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -8,6 +8,7 @@ from frappe.utils import nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestAdditionalSalary(unittest.TestCase): @@ -15,12 +16,19 @@ class TestAdditionalSalary(unittest.TestCase): def setUp(self): setup_test() + def tearDown(self): + for dt in ["Salary Slip", "Additional Salary", "Salary Structure Assignment", "Salary Structure"]: + frappe.db.sql("delete from `tab%s`" % dt) + def test_recurring_additional_salary(self): + amount = 0 + salary_component = None emp_id = make_employee("test_additional@salary.com") frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) + salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) add_sal = get_additional_salary(emp_id) - - ss = make_employee_salary_slip("test_additional@salary.com", "Monthly") + + ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) for earning in ss.earnings: if earning.salary_component == "Recurring Salary Component": amount = earning.amount @@ -29,8 +37,6 @@ class TestAdditionalSalary(unittest.TestCase): self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) - - def get_additional_salary(emp_id): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") @@ -40,6 +46,7 @@ def get_additional_salary(emp_id): add_sal.from_date = add_days(nowdate(), -50) add_sal.to_date = add_days(nowdate(), 180) add_sal.amount = 5000 + add_sal.currency = erpnext.get_default_currency() add_sal.save() add_sal.submit() diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js index f509df31e8..6756cd93e7 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js @@ -3,7 +3,12 @@ frappe.ui.form.on('Employee Benefit Application', { employee: function(frm) { - frm.trigger('set_earning_component'); + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_earning_component') + ]); + } var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; @@ -38,9 +43,26 @@ frappe.ui.form.on('Employee Benefit Application', { }); }, + get_employee_currency: function(frm) { + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + } + }, + payroll_period: function(frm) { var method, args; - if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ + if (frm.doc.employee && frm.doc.date && frm.doc.payroll_period) { method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; args = { employee: frm.doc.employee, @@ -60,11 +82,14 @@ var get_max_benefits=function(frm, method, args) { method: method, args: args, callback: function (data) { - if(!data.exc){ - if(data.message){ + if (!data.exc) { + if (data.message) { frm.set_value("max_benefits", data.message); + } else { + frm.set_value("max_benefits", 0); } } + frm.refresh_fields(); } }); }; @@ -82,14 +107,19 @@ var calculate_all = function(doc) { var tbl = doc.employee_benefits || []; var pro_rata_dispensed_amount = 0; var total_amount = 0; - for(var i = 0; i < tbl.length; i++){ - if(cint(tbl[i].amount) > 0) { - total_amount += flt(tbl[i].amount); - } - if(tbl[i].pay_against_benefit_claim != 1){ - pro_rata_dispensed_amount += flt(tbl[i].amount); + if (doc.max_benefits === 0) { + doc.employee_benefits = []; + } else { + for (var i = 0; i < tbl.length; i++) { + if (cint(tbl[i].amount) > 0) { + total_amount += flt(tbl[i].amount); + } + if (tbl[i].pay_against_benefit_claim != 1) { + pro_rata_dispensed_amount += flt(tbl[i].amount); + } } } + doc.total_amount = total_amount; doc.remaining_benefit = doc.max_benefits - total_amount; doc.pro_rata_dispensed_amount = pro_rata_dispensed_amount; diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index b0c1bd6c3e..9a5a463152 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -10,12 +10,14 @@ "field_order": [ "employee", "employee_name", + "currency", "max_benefits", "remaining_benefit", "column_break_2", "date", "payroll_period", "department", + "company", "amended_from", "section_break_4", "employee_benefits", @@ -43,12 +45,14 @@ "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Yearly)", + "options": "currency", "read_only": 1 }, { "fieldname": "remaining_benefit", "fieldtype": "Currency", "label": "Remaining Benefits (Yearly)", + "options": "currency", "read_only": 1 }, { @@ -108,18 +112,38 @@ "fieldname": "total_amount", "fieldtype": "Currency", "label": "Total Amount", + "options": "currency", "read_only": 1 }, { "fieldname": "pro_rata_dispensed_amount", "fieldtype": "Currency", "label": "Dispensed Amount (Pro-rated)", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:58:31.271922", + "modified": "2020-11-25 11:49:05.095101", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index ef844fbd3b..27df30a459 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -33,8 +33,8 @@ class EmployeeBenefitApplication(Document): benefit_given = get_sal_slip_total_benefit_given(self.employee, payroll_period, component = benefit.earning_component) benefit_claim_remining = benefit_claimed - benefit_given if benefit_claimed > 0 and benefit_claim_remining > benefit.amount: - frappe.throw(_("An amount of {0} already claimed for the component {1},\ - set the amount equal or greater than {2}").format(benefit_claimed, benefit.earning_component, benefit_claim_remining)) + frappe.throw(_("An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}").format( + benefit_claimed, benefit.earning_component, benefit_claim_remining)) def validate_remaining_benefit_amount(self): # check salary structure earnings have flexi component (sum of max_benefit_amount) @@ -62,11 +62,11 @@ class EmployeeBenefitApplication(Document): if pro_rata_amount == 0 and non_pro_rata_amount == 0: frappe.throw(_("Please add the remaining benefits {0} to any of the existing component").format(self.remaining_benefit)) elif non_pro_rata_amount > 0 and non_pro_rata_amount < rounded(self.remaining_benefit): - frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application \ - as pro-rata component").format(non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) + frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component").format( + non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) elif non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to the application as \ - pro-rata component").format(self.remaining_benefit)) + frappe.throw(_("Please add the remaining benefits {0} to the application as pro-rata component").format( + self.remaining_benefit)) def validate_max_benefit_for_component(self): if self.employee_benefits: @@ -115,7 +115,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): if max_benefits and max_benefits > 0: have_depends_on_payment_days = False per_day_amount_total = 0 - payroll_period_days = get_payroll_period_days(on_date, on_date, employee)[0] + payroll_period_days = get_payroll_period_days(on_date, on_date, employee)[1] payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period) # Get all salary slip flexi amount in the payroll period @@ -239,4 +239,17 @@ def get_earning_components(doctype, txt, searchfield, start, page_len, filters): """, salary_structure) else: frappe.throw(_("Salary Structure not found for employee {0} and date {1}") - .format(filters['employee'], filters['date'])) \ No newline at end of file + .format(filters['employee'], filters['date'])) + +@frappe.whitelist() +def get_earning_components_max_benefits(employee, date, earning_component): + salary_structure = get_assigned_salary_structure(employee, date) + amount = frappe.db.sql(""" + select amount + from `tabSalary Detail` + where parent = %s and is_flexible_benefit = 1 + and salary_component = %s + order by name + """, salary_structure, earning_component) + + return amount if amount else 0 \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json index fa6b4da2af..c93d356c20 100644 --- a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json +++ b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json @@ -33,6 +33,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Max Benefit Amount", + "options": "currency", "read_only": 1 }, { @@ -40,12 +41,13 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", + "options": "currency", "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:45:00.519134", + "modified": "2020-09-29 16:22:15.783854", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application Detail", diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js index 6db6cb86b3..ea9ccd5205 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js @@ -12,5 +12,24 @@ frappe.ui.form.on('Employee Benefit Claim', { }, employee: function(frm) { frm.set_value("earning_component", null); + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.set_df_property('currency', 'hidden', 0); + } + } + }); + } + if (!frm.doc.earning_component) { + frm.doc.max_amount_eligible = null; + frm.doc.claimed_amount = null; + } + frm.refresh_fields(); } }); diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json index ae4c218615..da24aacda1 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json @@ -12,6 +12,8 @@ "department", "column_break_3", "claim_date", + "currency", + "company", "benefit_type_and_amount", "earning_component", "max_amount_eligible", @@ -76,6 +78,7 @@ "fieldname": "max_amount_eligible", "fieldtype": "Currency", "label": "Max Amount Eligible", + "options": "currency", "read_only": 1 }, { @@ -92,6 +95,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Claimed Amount", + "options": "currency", "reqd": 1 }, { @@ -119,11 +123,29 @@ "fieldname": "attachments", "fieldtype": "Attach", "label": "Attachments" + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 23:01:50.791676", + "modified": "2020-11-25 11:49:56.097352", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Claim", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index db0f83aac9..85d1c54a22 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -11,12 +11,57 @@ frappe.ui.form.on('Employee Incentive', { }; }); + if (!frm.doc.currency) return; frm.set_query("salary_component", function() { return { - filters: { - "type": "Earning" - } + query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company} }; }); - } + + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.run_serially([ + () => frm.trigger('get_employee_currency'), + () => frm.trigger('set_company') + ]); + } else { + frm.set_value("company", null); + } + }, + + set_company: function(frm) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Employee", + fieldname: "company", + filters: { + name: frm.doc.employee + } + }, + callback: function(data) { + if (data.message) { + frm.set_value("company", data.message.company); + } + } + }); + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, }); diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json index 204c9a40b1..e5b1052b3a 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json @@ -7,10 +7,12 @@ "engine": "InnoDB", "field_order": [ "employee", - "incentive_amount", "employee_name", - "salary_component", + "company", + "currency", + "incentive_amount", "column_break_5", + "salary_component", "payroll_date", "department", "amended_from" @@ -28,6 +30,7 @@ "fieldname": "incentive_amount", "fieldtype": "Currency", "label": "Incentive Amount", + "options": "currency", "reqd": 1 }, { @@ -70,11 +73,29 @@ "label": "Salary Component", "options": "Salary Component", "reqd": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:42:51.209630", + "modified": "2020-10-20 17:22:16.468042", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Incentive", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index 84a97f6bb2..ead3db126f 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -4,14 +4,23 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.model.document import Document class EmployeeIncentive(Document): + def validate(self): + self.validate_salary_structure() + + def validate_salary_structure(self): + if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + def on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = self.employee + additional_salary.currency = self.currency additional_salary.salary_component = self.salary_component additional_salary.overwrite_salary_structure_amount = 0 additional_salary.amount = self.incentive_amount diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index de7c348bb2..83d4ae53df 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -14,6 +14,7 @@ "column_break_2", "payroll_period", "company", + "currency", "amended_from", "section_break_8", "declarations", @@ -92,6 +93,7 @@ "fieldname": "total_declared_amount", "fieldtype": "Currency", "label": "Total Declared Amount", + "options": "currency", "read_only": 1 }, { @@ -102,12 +104,22 @@ "fieldname": "total_exemption_amount", "fieldtype": "Currency", "label": "Total Exemption Amount", + "options": "currency", "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:49:43.829892", + "modified": "2020-10-20 16:42:24.493761", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 9549fd1b75..0609d19149 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -22,6 +22,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -39,6 +40,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -54,6 +56,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", @@ -70,6 +73,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), "declarations": [ dict(exemption_sub_category = "_Test Sub Category", exemption_category = "_Test Category", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json index 8c2f9aa370..723a3df3c7 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json @@ -35,6 +35,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Maximum Exempted Amount", + "options": "currency", "read_only": 1, "reqd": 1 }, @@ -43,12 +44,13 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Declared Amount", + "options": "currency", "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:41:03.638739", + "modified": "2020-10-20 16:43:09.606265", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration Category", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js index 715d7553b0..497f35c41e 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js @@ -54,5 +54,9 @@ frappe.ui.form.on('Employee Tax Exemption Proof Submission', { }); }); } + }, + + currency: function(frm) { + frm.refresh_fields(); } }); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index b62b5aab0b..53f18cb1fe 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -11,6 +11,7 @@ "employee", "employee_name", "department", + "currency", "column_break_2", "submission_date", "payroll_period", @@ -97,6 +98,7 @@ "fieldname": "total_actual_amount", "fieldtype": "Currency", "label": "Total Actual Amount", + "options": "currency", "read_only": 1 }, { @@ -107,6 +109,7 @@ "fieldname": "exemption_amount", "fieldtype": "Currency", "label": "Total Exemption Amount", + "options": "currency", "read_only": 1 }, { @@ -126,11 +129,20 @@ "options": "Employee Tax Exemption Proof Submission", "print_hide": 1, "read_only": 1 + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:53:10.412321", + "modified": "2020-10-20 16:47:03.410020", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json index c1f532050a..2fd8b94efd 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json @@ -34,6 +34,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Maximum Exemption Amount", + "options": "currency", "read_only": 1, "reqd": 1 }, @@ -48,12 +49,13 @@ "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Actual Amount" + "label": "Actual Amount", + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:37:08.265600", + "modified": "2020-10-20 16:47:31.480870", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission Detail", diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js index 73a54eb8dd..7d780d3b04 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js @@ -2,5 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Income Tax Slab', { - + currency: function(frm) { + frm.refresh_fields(); + } }); diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json index 6337d5a6d3..9fa261dea2 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json @@ -9,8 +9,9 @@ "effective_from", "company", "column_break_3", - "allow_tax_exemption", + "currency", "standard_tax_exemption_amount", + "allow_tax_exemption", "disabled", "amended_from", "taxable_salary_slabs_section", @@ -70,7 +71,7 @@ "fieldname": "standard_tax_exemption_amount", "fieldtype": "Currency", "label": "Standard Tax Exemption Amount", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "company", @@ -90,11 +91,20 @@ "fieldtype": "Table", "label": "Other Taxes and Charges", "options": "Income Tax Slab Other Charges" + }, + { + "default": "Company:company:default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 20:27:13.425084", + "modified": "2020-10-19 13:54:24.728075", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab", diff --git a/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json index 7f21204591..0dba338250 100644 --- a/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json +++ b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -45,7 +45,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Min Taxable Income", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "column_break_7", @@ -57,12 +57,12 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Max Taxable Income", - "options": "Company:company:default_currency" + "options": "currency" } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:33:17.931912", + "modified": "2020-10-19 13:45:12.850090", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab Other Charges", diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json index bb68e1814a..8a55224dca 100644 --- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -52,7 +52,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-22 23:25:13.779032", + "modified": "2020-09-30 12:40:07.999878", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Employee Detail", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 1abc869c53..cb48abbc36 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -17,6 +17,16 @@ frappe.ui.form.on('Payroll Entry', { } }; }); + + frm.set_query("payroll_payable_account", function() { + return { + filters: { + "company": frm.doc.company, + "root_type": "Liability", + "is_group": 0, + } + }; + }); }, refresh: function(frm) { @@ -139,6 +149,36 @@ frappe.ui.form.on('Payroll Entry', { frm.events.clear_employee_table(frm); }, + currency: function (frm) { + var company_currency; + if (!frm.doc.company) { + company_currency = erpnext.get_currency(frappe.defaults.get_default("Company")); + } else { + company_currency = erpnext.get_currency(frm.doc.company); + } + if (frm.doc.currency) { + if (company_currency != frm.doc.currency) { + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: frm.doc.currency, + to_currency: company_currency, + }, + callback: function(r) { + frm.set_value("exchange_rate", flt(r.message)); + frm.set_df_property('exchange_rate', 'hidden', 0); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); + } + }); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + } + }, + department: function (frm) { frm.events.clear_employee_table(frm); }, diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 31a899699d..7a48dd1475 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -11,8 +11,11 @@ "column_break0", "posting_date", "payroll_frequency", - "column_break1", "company", + "column_break1", + "currency", + "exchange_rate", + "payroll_payable_account", "section_break_8", "branch", "department", @@ -257,12 +260,37 @@ { "fieldname": "column_break_33", "fieldtype": "Column Break" + }, + { + "depends_on": "company", + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "depends_on": "company", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9", + "reqd": 1 + }, + { + "depends_on": "company", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "options": "Account", + "reqd": 1 } ], "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-06-22 20:06:06.953904", + "modified": "2020-10-23 13:00:33.753228", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index a3d12c35c0..67ee231e40 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.model.document import Document from dateutil.relativedelta import relativedelta from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff @@ -51,13 +51,15 @@ class PayrollEntry(Document): where docstatus = 1 and is_active = 'Yes' - and company = %(company)s and + and company = %(company)s + and currency = %(currency)s and ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s {condition}""".format(condition=condition), - {"company": self.company, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) + {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " + cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " cond += "and %(from_date)s >= t2.from_date" emp_list = frappe.db.sql(""" select @@ -68,14 +70,26 @@ class PayrollEntry(Document): t1.name = t2.employee and t2.docstatus = 1 %s order by t2.from_date desc - """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date}, as_dict=True) + """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) return emp_list def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() if not employees: - frappe.throw(_("No employees for the mentioned criteria")) + error_msg = _("No employees found for the mentioned criteria:
Company: {0}
Currency: {1}
Payroll Payable Account: {2}").format( + frappe.bold(self.company), frappe.bold(self.currency), frappe.bold(self.payroll_payable_account)) + if self.branch: + error_msg += "
" + _("Branch: {0}").format(frappe.bold(self.branch)) + if self.department: + error_msg += "
" + _("Department: {0}").format(frappe.bold(self.department)) + if self.designation: + error_msg += "
" + _("Designation: {0}").format(frappe.bold(self.designation)) + if self.start_date: + error_msg += "
" + _("Start date: {0}").format(frappe.bold(self.start_date)) + if self.end_date: + error_msg += "
" + _("End date: {0}").format(frappe.bold(self.end_date)) + frappe.throw(error_msg, title=_("No employees found")) for d in employees: self.append('employees', d) @@ -123,7 +137,9 @@ class PayrollEntry(Document): "posting_date": self.posting_date, "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, - "payroll_entry": self.name + "payroll_entry": self.name, + "exchange_rate": self.exchange_rate, + "currency": self.currency }) if len(emp_list) > 30: frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) @@ -160,10 +176,10 @@ class PayrollEntry(Document): def get_salary_component_account(self, salary_component): account = frappe.db.get_value("Salary Component Account", - {"parent": salary_component, "company": self.company}, "default_account") + {"parent": salary_component, "company": self.company}, "account") if not account: - frappe.throw(_("Please set default account in Salary Component {0}") + frappe.throw(_("Please set account in Salary Component {0}") .format(salary_component)) return account @@ -203,21 +219,11 @@ class PayrollEntry(Document): account_dict[(account, key[1])] = account_dict.get((account, key[1]), 0) + amount return account_dict - def get_default_payroll_payable_account(self): - payroll_payable_account = frappe.get_cached_value('Company', - {"company_name": self.company}, "default_payroll_payable_account") - - if not payroll_payable_account: - frappe.throw(_("Please set Default Payroll Payable Account in Company {0}") - .format(self.company)) - - return payroll_payable_account - def make_accrual_jv_entry(self): self.check_permission('write') earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} - default_payroll_payable_account = self.get_default_payroll_payable_account() + payroll_payable_account = self.payroll_payable_account jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -230,14 +236,19 @@ class PayrollEntry(Document): journal_entry.posting_date = self.posting_date accounts = [] + currencies = [] payable_amount = 0 + multi_currency = 0 + company_currency = erpnext.get_company_currency(self.company) # Earnings for acc_cc, amount in earnings.items(): + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount += flt(amount, precision) accounts.append({ "account": acc_cc[0], - "debit_in_account_currency": flt(amount, precision), + "debit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), "party_type": '', "cost_center": acc_cc[1] or self.cost_center, "project": self.project @@ -245,25 +256,32 @@ class PayrollEntry(Document): # Deductions for acc_cc, amount in deductions.items(): + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount -= flt(amount, precision) accounts.append({ "account": acc_cc[0], - "credit_in_account_currency": flt(amount, precision), + "credit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), "cost_center": acc_cc[1] or self.cost_center, "party_type": '', "project": self.project }) # Payable amount + exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) accounts.append({ - "account": default_payroll_payable_account, - "credit_in_account_currency": flt(payable_amount, precision), + "account": payroll_payable_account, + "credit_in_account_currency": flt(payable_amt, precision), + "exchange_rate": flt(exchange_rate), "party_type": '', "cost_center": self.cost_center }) journal_entry.set("accounts", accounts) - journal_entry.title = default_payroll_payable_account + if len(currencies) > 1: + multi_currency = 1 + journal_entry.multi_currency = multi_currency + journal_entry.title = payroll_payable_account journal_entry.save() try: @@ -275,6 +293,18 @@ class PayrollEntry(Document): return jv_name + def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): + conversion_rate = 1 + exchange_rate = self.exchange_rate + account_currency = frappe.db.get_value('Account', account, 'account_currency') + if account_currency not in currencies: + currencies.append(account_currency) + if account_currency == company_currency: + conversion_rate = self.exchange_rate + exchange_rate = 1 + amount = flt(amount) * flt(conversion_rate) + return exchange_rate, amount + def make_payment_entry(self): self.check_permission('write') @@ -303,31 +333,43 @@ class PayrollEntry(Document): self.create_journal_entry(salary_slip_total, "salary") def create_journal_entry(self, je_payment_amount, user_remark): - default_payroll_payable_account = self.get_default_payroll_payable_account() + payroll_payable_account = self.payroll_payable_account precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") + accounts = [] + currencies = [] + multi_currency = 0 + company_currency = erpnext.get_company_currency(self.company) + + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) + accounts.append({ + "account": self.payment_account, + "bank_account": self.bank_account, + "credit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + }) + + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) + accounts.append({ + "account": payroll_payable_account, + "debit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + "reference_type": self.doctype, + "reference_name": self.name + }) + + if len(currencies) > 1: + multi_currency = 1 + journal_entry = frappe.new_doc('Journal Entry') journal_entry.voucher_type = 'Bank Entry' journal_entry.user_remark = _('Payment of {0} from {1} to {2}')\ .format(user_remark, self.start_date, self.end_date) journal_entry.company = self.company journal_entry.posting_date = self.posting_date + journal_entry.multi_currency = multi_currency - payment_amount = flt(je_payment_amount, precision) - - journal_entry.set("accounts", [ - { - "account": self.payment_account, - "bank_account": self.bank_account, - "credit_in_account_currency": payment_amount - }, - { - "account": default_payroll_payable_account, - "debit_in_account_currency": payment_amount, - "reference_type": self.doctype, - "reference_name": self.name - } - ]) + journal_entry.set("accounts", accounts) journal_entry.save(ignore_permissions = True) def update_salary_slip_status(self, jv_name = None): @@ -496,6 +538,21 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): if publish_progress: frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), title = _("Creating Salary Slips...")) + else: + salary_slip_name = frappe.db.sql( + '''SELECT + name + FROM `tabSalary Slip` + WHERE company=%s + AND start_date >= %s + AND end_date <= %s + AND employee = %s + ''', (args.company, args.start_date, args.end_date, emp), as_dict=True) + + salary_slip_doc = frappe.get_doc('Salary Slip', salary_slip_name[0].name) + salary_slip_doc.exchange_rate = args.exchange_rate + salary_slip_doc.set_totals() + salary_slip_doc.db_update() payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) payroll_entry.db_set("salary_slips_created", 1) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index b0f225d909..54106c8d16 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -10,8 +10,8 @@ from frappe.utils import add_months from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ - make_earning_salary_component, make_deduction_salary_component, create_account -from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + make_earning_salary_component, make_deduction_salary_component, create_account, make_employee_salary_slip +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans @@ -34,10 +34,47 @@ class TestPayrollEntry(unittest.TestCase): get_salary_component_account(data.name) employee = frappe.db.get_value("Employee", {'company': company}) - make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company) + company_doc = frappe.get_doc('Company', company) + make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company, currency=company_doc.default_currency) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date) + make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency) + + def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use + company = erpnext.get_default_company() + employee = make_employee("test_muti_currency_employee@payroll.com", company=company) + for data in frappe.get_all('Salary Component', fields = ["name"]): + if not frappe.db.get_value('Salary Component Account', + {'parent': data.name, 'company': company}, 'name'): + get_salary_component_account(data.name) + + company_doc = frappe.get_doc('Company', company) + salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD') + create_salary_structure_assignment(employee, salary_structure.name, company=company) + frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"}))) + salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure") + dates = get_start_end_dates('Monthly', nowdate()) + payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70) + payroll_entry.make_payment_entry() + + salary_slip.load_from_db() + + payroll_je = salary_slip.journal_entry + payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je) + + self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) + self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) + + payment_entry = frappe.db.sql(''' + Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea + Where je.name = jea.parent + And jea.reference_name = %s + ''', (payroll_entry.name), as_dict=1) + + self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) + self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use for data in frappe.get_all('Salary Component', fields = ["name"]): @@ -52,24 +89,32 @@ class TestPayrollEntry(unittest.TestCase): "company": "_Test Company" }).insert() + frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """) + frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """) + frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """) + frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """) + employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", department="cc - _TC", company="_Test Company") employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", department="cc - _TC", company="_Test Company") - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company") - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company") - if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): - create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC") - frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", - "_Test Payroll Payable - _TC") + create_account(account_name="_Test Payroll Payable", + company="_Test Company", parent_account="Current Liabilities - _TC") + + if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ + frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": + frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", + "_Test Payroll Payable - _TC") + + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, - department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") + pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account="_Test Payroll Payable - _TC", + currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") je_entries = frappe.db.sql(""" select account, cost_center, debit, credit @@ -121,7 +166,7 @@ class TestPayrollEntry(unittest.TestCase): employee_doc.save() salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company") + make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 @@ -133,8 +178,8 @@ class TestPayrollEntry(unittest.TestCase): dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(company="_Test Company", start_date=dates.start_date, - end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") + make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") name = frappe.db.get_value('Salary Slip', {'posting_date': nowdate(), 'employee': applicant}, 'name') @@ -165,6 +210,9 @@ def make_payroll_entry(**args): payroll_entry.payroll_frequency = "Monthly" payroll_entry.branch = args.branch or None payroll_entry.department = args.department or None + payroll_entry.payroll_payable_account = args.payable_account + payroll_entry.currency = args.currency + payroll_entry.exchange_rate = args.exchange_rate or 1 if args.cost_center: payroll_entry.cost_center = args.cost_center @@ -212,3 +260,11 @@ def make_holiday(holiday_list_name): }).insert() return holiday_list_name + +def get_salary_slip(user, period, salary_structure): + salary_slip = make_employee_salary_slip(user, period, salary_structure) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + salary_slip.db_update() + + return salary_slip \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js index 8ff55151f6..092cbd8974 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js +++ b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js @@ -9,45 +9,45 @@ QUnit.test("test: Set Salary Components", function (assert) { () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Basic'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Income Tax'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Salary Component', 'Arrear'), () => { var row = frappe.model.add_child(cur_frm.doc, "Salary Component Account", "accounts"); row.company = 'For Testing'; - row.default_account = 'Salary - FT'; + row.account = 'Salary - FT'; }, () => cur_frm.save(), () => frappe.timeout(2), - () => assert.equal(cur_frm.doc.accounts[0].default_account, 'Salary - FT'), + () => assert.equal(cur_frm.doc.accounts[0].account, 'Salary - FT'), () => frappe.set_route('Form', 'Company', 'For Testing'), () => cur_frm.set_value('default_payroll_payable_account', 'Payroll Payable - FT'), diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js index 64e726db85..6fe8ccad46 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js @@ -18,5 +18,22 @@ frappe.ui.form.on('Retention Bonus', { } }; }); + }, + + employee: function(frm) { + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + } } }); diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json index da884c2f28..6647230078 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json @@ -17,7 +17,8 @@ "column_break_6", "employee_name", "department", - "date_of_joining" + "date_of_joining", + "currency" ], "fields": [ { @@ -46,6 +47,7 @@ "fieldname": "bonus_amount", "fieldtype": "Currency", "label": "Bonus Amount", + "options": "currency", "reqd": 1 }, { @@ -89,11 +91,22 @@ "label": "Salary Component", "options": "Salary Component", "reqd": 1 + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.employee)", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 22:42:05.251951", + "modified": "2020-10-20 17:27:47.003134", "modified_by": "Administrator", "module": "Payroll", "name": "Retention Bonus", @@ -151,7 +164,6 @@ "share": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index c455eb3303..dbf75140ac 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { - frm.set_query("default_account", "accounts", function(doc, cdt, cdn) { + frm.set_query("account", "accounts", function(doc, cdt, cdn) { var d = locals[cdt][cdn]; return { filters: { diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index eedb56ec08..5c1eb61281 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -147,7 +147,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency" + "options": "currency" }, { "default": "0", @@ -160,7 +160,7 @@ "fieldname": "default_amount", "fieldtype": "Currency", "label": "Default Amount", - "options": "Company:company:default_currency", + "options": "currency", "print_hide": 1 }, { @@ -169,6 +169,7 @@ "hidden": 1, "label": "Additional Amount", "no_copy": 1, + "options": "currency", "print_hide": 1, "read_only": 1 }, @@ -177,6 +178,7 @@ "fieldname": "tax_on_flexible_benefit", "fieldtype": "Currency", "label": "Tax on flexible benefit", + "options": "currency", "read_only": 1 }, { @@ -184,6 +186,7 @@ "fieldname": "tax_on_additional_salary", "fieldtype": "Currency", "label": "Tax on additional salary", + "options": "currency", "read_only": 1 }, { @@ -227,7 +230,7 @@ ], "istable": 1, "links": [], - "modified": "2020-10-07 20:39:41.619283", + "modified": "2020-11-25 13:12:41.081106", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 0671b570d1..f7e22c6387 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -74,14 +74,85 @@ frappe.ui.form.on("Salary Slip", { if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } + frm.trigger("set_dynamic_labels"); + }, + + set_dynamic_labels: function(frm) { + var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency"); + frappe.run_serially([ + () => frm.events.set_exchange_rate(frm, company_currency), + () => frm.events.change_form_labels(frm, company_currency), + () => frm.events.change_grid_labels(frm), + () => frm.refresh_fields() + ]); + }, + + set_exchange_rate: function(frm, company_currency) { + if (frm.doc.docstatus === 0) { + if (frm.doc.currency) { + var from_currency = frm.doc.currency; + if (from_currency != company_currency) { + frm.events.hide_loan_section(frm); + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: from_currency, + to_currency: company_currency, + }, + callback: function(r) { + frm.set_value("exchange_rate", flt(r.message)); + frm.set_df_property('exchange_rate', 'hidden', 0); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); + } + }); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property('exchange_rate', 'hidden', 1); + frm.set_df_property("exchange_rate", "description", "" ); + } + } + } + }, + + exchange_rate: function(frm) { + calculate_totals(frm); + }, + + hide_loan_section: function(frm) { + frm.set_df_property('section_break_43', 'hidden', 1); + }, + + change_form_labels: function(frm, company_currency) { + frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", + "base_net_pay", "base_rounded_total", "base_total_in_words"], + company_currency); + + frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], + frm.doc.currency); + + // toggle fields + frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", + "base_net_pay", "base_rounded_total", "base_total_in_words"], + frm.doc.currency != company_currency); + }, + + change_grid_labels: function(frm) { + frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"], frm.doc.currency, "earnings"); + + frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"], frm.doc.currency, "deductions"); }, refresh: function(frm) { frm.trigger("toggle_fields"); var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; - cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); - cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); + frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); + frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); + calculate_totals(frm); + frm.trigger("set_dynamic_labels"); }, salary_slip_based_on_timesheet: function(frm) { @@ -118,51 +189,94 @@ frappe.ui.form.on("Salary Slip", { }, get_emp_and_working_day_details: function(frm) { - return frappe.call({ - method: 'get_emp_and_working_day_details', - doc: frm.doc, - callback: function(r) { - frm.refresh(); - if (r.message[1] !== "Leave" && r.message[0]) { - frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as ")+ r.message[0] +__(". You can can change this in ") + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); + if (frm.doc.employee) { + return frappe.call({ + method: 'get_emp_and_working_day_details', + doc: frm.doc, + callback: function(r) { + if (r.message[1] !== "Leave" && r.message[0]) { + frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as {0}. You can can change this in {1}", [r.message, frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)])); + } + frm.refresh(); } - } - }); + }); + } } }); frappe.ui.form.on('Salary Slip Timesheet', { - time_sheet: function(frm, dt, dn) { - total_work_hours(frm, dt, dn); + time_sheet: function(frm) { + calculate_totals(frm); }, - timesheets_remove: function(frm, dt, dn) { - total_work_hours(frm, dt, dn); + timesheets_remove: function(frm) { + calculate_totals(frm); } }); -// calculate total working hours, earnings based on hourly wages and totals -var total_work_hours = function(frm) { - var total_working_hours = 0.0; - $.each(frm.doc["timesheets"] || [], function(i, timesheet) { - total_working_hours += timesheet.working_hours; - }); - frm.set_value('total_working_hours', total_working_hours); - - var wages_amount = frm.doc.total_working_hours * frm.doc.hour_rate; - - frappe.db.get_value('Salary Structure', {'name': frm.doc.salary_structure}, 'salary_component', (r) => { - var gross_pay = 0.0; - $.each(frm.doc["earnings"], function(i, earning) { - if (earning.salary_component == r.salary_component) { - earning.amount = wages_amount; - frm.refresh_fields('earnings'); +var calculate_totals = function(frm) { + if (frm.doc.earnings || frm.doc.deductions) { + frappe.call({ + method: "set_totals", + doc: frm.doc, + callback: function() { + frm.refresh_fields(); } - gross_pay += earning.amount; }); - frm.set_value('gross_pay', gross_pay); - - frm.doc.net_pay = flt(frm.doc.gross_pay) - flt(frm.doc.total_deduction); - frm.doc.rounded_total = Math.round(frm.doc.net_pay); - refresh_many(['net_pay', 'rounded_total']); - }); + } }; + +frappe.ui.form.on('Salary Detail', { + amount: function(frm) { + calculate_totals(frm); + }, + + earnings_remove: function(frm) { + calculate_totals(frm); + }, + + deductions_remove: function(frm) { + calculate_totals(frm); + }, + + salary_component: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.salary_component) { + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Salary Component", + name: child.salary_component + }, + callback: function(data) { + if (data.message) { + var result = data.message; + frappe.model.set_value(cdt, cdn, 'condition', result.condition); + frappe.model.set_value(cdt, cdn, 'amount_based_on_formula', result.amount_based_on_formula); + if (result.amount_based_on_formula === 1) { + frappe.model.set_value(cdt, cdn, 'formula', result.formula); + } else { + frappe.model.set_value(cdt, cdn, 'amount', result.amount); + } + frappe.model.set_value(cdt, cdn, 'statistical_component', result.statistical_component); + frappe.model.set_value(cdt, cdn, 'depends_on_payment_days', result.depends_on_payment_days); + frappe.model.set_value(cdt, cdn, 'do_not_include_in_total', result.do_not_include_in_total); + frappe.model.set_value(cdt, cdn, 'variable_based_on_taxable_salary', result.variable_based_on_taxable_salary); + frappe.model.set_value(cdt, cdn, 'is_tax_applicable', result.is_tax_applicable); + frappe.model.set_value(cdt, cdn, 'is_flexible_benefit', result.is_flexible_benefit); + refresh_field("earnings"); + refresh_field("deductions"); + } + } + }); + } + }, + + amount_based_on_formula: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if (child.amount_based_on_formula === 1) { + frappe.model.set_value(cdt, cdn, 'amount', null); + } else { + frappe.model.set_value(cdt, cdn, 'formula', null); + } + } +}); diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 619c45fa4a..386618cf08 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -18,6 +18,8 @@ "journal_entry", "payroll_entry", "company", + "currency", + "exchange_rate", "letter_head", "section_break_10", "start_date", @@ -38,6 +40,7 @@ "column_break_20", "total_working_hours", "hour_rate", + "base_hour_rate", "section_break_26", "bank_name", "bank_account_no", @@ -52,8 +55,10 @@ "deductions", "totals", "gross_pay", + "base_gross_pay", "column_break_25", "total_deduction", + "base_total_deduction", "loan_repayment", "loans", "section_break_43", @@ -63,10 +68,15 @@ "total_loan_repayment", "net_pay_info", "net_pay", + "base_net_pay", "column_break_53", "rounded_total", + "base_rounded_total", "section_break_55", "total_in_words", + "column_break_69", + "base_total_in_words", + "section_break_75", "amended_from" ], "fields": [ @@ -205,9 +215,13 @@ { "fieldname": "salary_structure", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Salary Structure", "options": "Salary Structure", - "read_only": 1 + "read_only": 1, + "reqd": 1, + "search_index": 1 }, { "depends_on": "eval:(!doc.salary_slip_based_on_timesheet)", @@ -265,7 +279,7 @@ "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency", + "options": "currency", "print_hide_if_no_value": 1 }, { @@ -347,24 +361,13 @@ "fieldname": "gross_pay", "fieldtype": "Currency", "label": "Gross Pay", - "oldfieldname": "gross_pay", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { "fieldname": "column_break_25", "fieldtype": "Column Break" }, - { - "fieldname": "total_deduction", - "fieldtype": "Currency", - "label": "Total Deduction", - "oldfieldname": "total_deduction", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "depends_on": "total_loan_repayment", "fieldname": "loan_repayment", @@ -379,6 +382,7 @@ "print_hide": 1 }, { + "depends_on": "eval:doc.docstatus != 0", "fieldname": "section_break_43", "fieldtype": "Section Break" }, @@ -416,13 +420,10 @@ "label": "net pay info" }, { - "description": "Gross Pay - Total Deduction - Loan Repayment", "fieldname": "net_pay", "fieldtype": "Currency", "label": "Net Pay", - "oldfieldname": "net_pay", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { @@ -434,22 +435,13 @@ "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", - "options": "Company:company:default_currency", + "options": "currency", "read_only": 1 }, { "fieldname": "section_break_55", "fieldtype": "Section Break" }, - { - "description": "Net Pay (in words) will be visible once you save the Salary Slip.", - "fieldname": "total_in_words", - "fieldtype": "Data", - "label": "Total in words", - "oldfieldname": "net_pay_in_words", - "oldfieldtype": "Data", - "read_only": 1 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -500,13 +492,99 @@ { "fieldname": "column_break_18", "fieldtype": "Column Break" + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", + "fetch_from": "salary_structure.currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "total_deduction", + "fieldtype": "Currency", + "label": "Total Deduction", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "total_in_words", + "fieldtype": "Data", + "label": "Total in words", + "length": 240, + "read_only": 1 + }, + { + "fieldname": "section_break_75", + "fieldtype": "Section Break" + }, + { + "fieldname": "base_hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide_if_no_value": 1 + }, + { + "fieldname": "base_gross_pay", + "fieldtype": "Currency", + "label": "Gross Pay (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "1.0", + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "base_total_deduction", + "fieldtype": "Currency", + "label": "Total Deduction (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_net_pay", + "fieldtype": "Currency", + "label": "Net Pay (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "base_rounded_total", + "fieldtype": "Currency", + "label": "Rounded Total (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "base_total_in_words", + "fieldtype": "Data", + "label": "Total in words (Company Currency)", + "length": 240, + "read_only": 1 + }, + { + "fieldname": "column_break_69", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-08-11 17:37:54.274384", + "modified": "2020-10-21 23:02:59.400249", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 7b87ae5e7b..20365b191d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -50,16 +50,20 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() - company_currency = erpnext.get_company_currency(self.company) - total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total - self.total_in_words = money_in_words(total, company_currency) - if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)): frappe.msgprint(_("Total working hours should not be greater than max working hours {0}"). format(max_working_hours), alert=True) + def set_net_total_in_words(self): + doc_currency = self.currency + company_currency = erpnext.get_company_currency(self.company) + total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total + base_total = self.base_net_pay if self.is_rounding_total_disabled() else self.base_rounded_total + self.total_in_words = money_in_words(total, doc_currency) + self.base_total_in_words = money_in_words(base_total, company_currency) + def on_submit(self): if self.net_pay < 0: frappe.throw(_("Net Pay cannot be less than 0")) @@ -182,6 +186,7 @@ class SalarySlip(TransactionBase): if self.salary_slip_based_on_timesheet: self.salary_structure = self._salary_structure_doc.name self.hour_rate = self._salary_structure_doc.hour_rate + self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0 wages_amount = self.hour_rate * self.total_working_hours @@ -417,15 +422,22 @@ class SalarySlip(TransactionBase): if self.salary_structure: self.calculate_component_amounts("earnings") self.gross_pay = self.get_component_totals("earnings") + self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) if self.salary_structure: self.calculate_component_amounts("deductions") self.total_deduction = self.get_component_totals("deductions") + self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) self.set_loan_repayment() self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) + self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) + self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision('base_net_pay')) + if self.hour_rate: + self.base_hour_rate = flt(flt(self.hour_rate) * flt(self.exchange_rate), self.precision('base_hour_rate')) + self.set_net_total_in_words() def calculate_component_amounts(self, component_type): if not getattr(self, '_salary_structure_doc', None): @@ -976,8 +988,9 @@ class SalarySlip(TransactionBase): amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") total_amount = amounts['interest_amount'] + amounts['payable_principal_amount'] if payment.total_payment > total_amount: - frappe.throw(_("Row {0}: Paid amount {1} is greater than pending accrued amount {2}against loan {3}").format( - payment.idx, frappe.bold(payment.total_payment),frappe.bold(total_amount), frappe.bold(payment.loan))) + frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""") + .format(payment.idx, frappe.bold(payment.total_payment), + frappe.bold(total_amount), frappe.bold(payment.loan))) self.total_interest_amount += payment.interest_amount self.total_principal_amount += payment.principal_amount @@ -1072,6 +1085,46 @@ class SalarySlip(TransactionBase): self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() + def set_totals(self): + self.gross_pay = 0 + if self.salary_slip_based_on_timesheet == 1: + self.calculate_total_for_salary_slip_based_on_timesheet() + else: + self.total_deduction = 0 + if self.earnings: + for earning in self.earnings: + self.gross_pay += flt(earning.amount) + if self.deductions: + for deduction in self.deductions: + self.total_deduction += flt(deduction.amount) + self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) + self.set_base_totals() + + def set_base_totals(self): + self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate) + self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate) + self.rounded_total = rounded(self.net_pay) + self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate) + self.base_rounded_total = rounded(self.base_net_pay) + self.set_net_total_in_words() + + #calculate total working hours, earnings based on hourly wages and totals + def calculate_total_for_salary_slip_based_on_timesheet(self): + if self.timesheets: + for timesheet in self.timesheets: + if timesheet.working_hours: + self.total_working_hours += timesheet.working_hours + + wages_amount = self.total_working_hours * self.hour_rate + self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) + salary_component = frappe.db.get_value('Salary Structure', {'name': self.salary_structure}, 'salary_component') + if self.earnings: + for i, earning in enumerate(self.earnings): + if earning.salary_component == salary_component: + self.earnings[i].amount = wages_amount + self.gross_pay += self.earnings[i].amount + self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index e08dc7c9c8..71cb4083ed 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -33,7 +33,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75) - emp_id = make_employee("test_for_attendance@salary.com") + emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) @@ -55,7 +55,7 @@ class TestSalarySlip(unittest.TestCase): mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp - ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days_based_on_attendance@salary.com", "Monthly", "Test Payment Based On Attendence") self.assertEqual(ss.leave_without_pay, 1.25) self.assertEqual(ss.absent_days, 1) @@ -78,7 +78,7 @@ class TestSalarySlip(unittest.TestCase): # Payroll based on attendance frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - emp_id = make_employee("test_for_attendance@salary.com") + emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) @@ -108,7 +108,8 @@ class TestSalarySlip(unittest.TestCase): #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave") - ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days_based_on_leave_application@salary.com", "Monthly", "Test Payment Based On Leave Application") + self.assertEqual(ss.leave_without_pay, 4) @@ -127,12 +128,12 @@ class TestSalarySlip(unittest.TestCase): def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) - make_employee("test_employee@salary.com") + make_employee("test_salary_slip_with_holidays_included@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "status", "Active") + ss = make_employee_salary_slip("test_salary_slip_with_holidays_included@salary.com", "Monthly", "Test Salary Slip With Holidays Included") self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, no_of_days[0]) @@ -143,12 +144,12 @@ class TestSalarySlip(unittest.TestCase): def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) - make_employee("test_employee@salary.com") + make_employee("test_salary_slip_with_holidays_excluded@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "status", "Active") + ss = make_employee_salary_slip("test_salary_slip_with_holidays_excluded@salary.com", "Monthly", "Test Salary Slip With Holidays Excluded") self.assertEqual(ss.total_working_days, no_of_days[0] - no_of_days[1]) self.assertEqual(ss.payment_days, no_of_days[0] - no_of_days[1]) @@ -163,7 +164,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) # set joinng date in the same month - make_employee("test_employee@salary.com") + make_employee("test_payment_days@salary.com") if getdate(nowdate()).day >= 15: relieving_date = getdate(add_days(nowdate(),-10)) date_of_joining = getdate(add_days(nowdate(),-10)) @@ -178,39 +179,39 @@ class TestSalarySlip(unittest.TestCase): relieving_date = getdate(nowdate()) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", date_of_joining) + {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", date_of_joining) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", "Test Payment Days") self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1)) # set relieving date in the same month frappe.db.set_value("Employee",frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) + {"employee_name":"test_payment_days@salary.com"}, "name"), "date_of_joining", (add_days(nowdate(),-60))) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", relieving_date) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", relieving_date) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Left") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Left") ss.save() self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, getdate(relieving_date).day) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) + {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active") + {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") def test_employee_salary_slip_read_permission(self): - make_employee("test_employee@salary.com") + make_employee("test_employee_salary_slip_read_permission@salary.com") - salary_slip_test_employee = make_employee_salary_slip("test_employee@salary.com", "Monthly") - frappe.set_user("test_employee@salary.com") + salary_slip_test_employee = make_employee_salary_slip("test_employee_salary_slip_read_permission@salary.com", "Monthly", "Test Employee Salary Slip Read Permission") + frappe.set_user("test_employee_salary_slip_read_permission@salary.com") self.assertTrue(salary_slip_test_employee.has_permission("read")) def test_email_salary_slip(self): @@ -218,8 +219,8 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) - make_employee("test_employee@salary.com") - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + make_employee("test_email_salary_slip@salary.com") + ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" ss.save() ss.submit() @@ -230,8 +231,9 @@ class TestSalarySlip(unittest.TestCase): def test_loan_repayment_salary_slip(self): from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - applicant = make_employee("test_loanemployee@salary.com", company="_Test Company") + applicant = make_employee("test_loan_repayment_salary_slip@salary.com", company="_Test Company") create_loan_accounts() @@ -243,6 +245,8 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') + make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') + frappe.db.sql("""delete from `tabLoan""") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() @@ -251,7 +255,7 @@ class TestSalarySlip(unittest.TestCase): process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly") + ss = make_employee_salary_slip("test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure") ss.submit() self.assertEqual(ss.total_loan_repayment, 592) @@ -264,7 +268,7 @@ class TestSalarySlip(unittest.TestCase): for payroll_frequency in ["Monthly", "Bimonthly", "Fortnightly", "Weekly", "Daily"]: make_employee(payroll_frequency + "_test_employee@salary.com") - ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency) + ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency, payroll_frequency + "_Test Payroll Frequency") if payroll_frequency == "Monthly": self.assertEqual(ss.end_date, m['month_end_date']) elif payroll_frequency == "Bimonthly": @@ -279,6 +283,18 @@ class TestSalarySlip(unittest.TestCase): elif payroll_frequency == "Daily": self.assertEqual(ss.end_date, nowdate()) + def test_multi_currency_salary_slip(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company") + frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""") + salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD') + salary_slip = make_salary_slip(salary_structure.name, employee = applicant) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + + self.assertEqual(salary_slip.gross_pay, 78000) + self.assertEqual(salary_slip.base_gross_pay, 78000*70) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -399,16 +415,21 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" employee = frappe.db.get_value("Employee", {"user_id": user}) - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) - salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) + if not frappe.db.exists('Salary Structure', salary_structure): + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) + else: + salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure) + salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) - if not salary_slip: + if not salary_slip_name: salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) salary_slip.employee_name = frappe.get_value("Employee", {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name") salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() + else: + salary_slip = frappe.get_doc('Salary Slip', salary_slip_name) return salary_slip @@ -449,7 +470,7 @@ def get_salary_component_account(sal_comp, company_list=None): sal_comp.append("accounts", { "company": d, - "default_account": create_account(account_name, d, parent_account) + "account": create_account(account_name, d, parent_account) }) sal_comp.save() @@ -576,7 +597,8 @@ def create_exemption_declaration(employee, payroll_period): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "payroll_period": payroll_period, - "company": erpnext.get_default_company() + "company": erpnext.get_default_company(), + "currency": erpnext.get_default_currency() }) declaration.append("declarations", { "exemption_sub_category": "_Test Sub Category", @@ -591,7 +613,8 @@ def create_proof_submission(employee, payroll_period, amount): "doctype": "Employee Tax Exemption Proof Submission", "employee": employee, "payroll_period": payroll_period.name, - "submission_date": submission_date + "submission_date": submission_date, + "currency": erpnext.get_default_currency() }) proof_submission.append("tax_exemption_proofs", { "exemption_sub_category": "_Test Sub Category", @@ -608,13 +631,13 @@ def create_benefit_claim(employee, payroll_period, amount, component): "employee": employee, "claimed_amount": amount, "claim_date": claim_date, - "earning_component": component + "earning_component": component, + "currency": erpnext.get_default_currency() }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False): - if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name): - return +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): + frappe.db.sql("""delete from `tabIncome Tax Slab`""") slabs = [ { @@ -637,6 +660,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + income_tax_slab.currency = currency if allow_tax_exemption: income_tax_slab.allow_tax_exemption = 1 @@ -687,7 +711,8 @@ def create_additional_salary(employee, payroll_period, amount): "salary_component": "Performance Bonus", "payroll_date": salary_date, "amount": amount, - "type": "Earning" + "type": "Earning", + "currency": erpnext.get_default_currency() }).submit() return salary_date diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index ad93a2fa4b..7daae49c58 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -41,20 +41,6 @@ frappe.ui.form.on('Salary Structure', { frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet) - frm.set_query("salary_component", "earnings", function() { - return { - filters: { - type: "earning" - } - } - }); - frm.set_query("salary_component", "deductions", function() { - return { - filters: { - type: "deduction" - } - } - }); frm.set_query("payment_account", function () { var account_types = ["Bank", "Cash"]; return { @@ -65,9 +51,48 @@ frappe.ui.form.on('Salary Structure', { } }; }); + frm.trigger('set_earning_deduction_component'); + }, + + set_earning_deduction_component: function(frm) { + if(!frm.doc.currency && !frm.doc.company) return; + frm.set_query("salary_component", "earnings", function() { + return { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company} + }; + }); + frm.set_query("salary_component", "deductions", function() { + return { + query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", + filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company} + }; + }); + }, + + + currency: function(frm) { + calculate_totals(frm.doc); + frm.trigger("set_dynamic_labels") + frm.trigger('set_earning_deduction_component'); + frm.refresh() + }, + + set_dynamic_labels: function(frm) { + frm.set_currency_labels(["net_pay","hour_rate", "leave_encashment_amount_per_day", "max_benefits", "total_earning", + "total_deduction"], frm.doc.currency); + + frm.set_currency_labels(["amount", "additional_amount", "tax_on_flexible_benefit", "tax_on_additional_salary"], + frm.doc.currency, "earnings"); + + frm.set_currency_labels(["amount", "additional_amount", "tax_on_flexible_benefit", "tax_on_additional_salary"], + frm.doc.currency, "deductions"); + + frm.refresh_fields(); }, refresh: function(frm) { + frm.trigger("set_dynamic_labels") frm.trigger("toggle_fields"); frm.fields_dict['earnings'].grid.set_column_disp("default_amount", false); frm.fields_dict['deductions'].grid.set_column_disp("default_amount", false); @@ -101,10 +126,12 @@ frappe.ui.form.on('Salary Structure', { fields: [ {fieldname: "sec_break", fieldtype: "Section Break", label: __("Filter Employees By (Optional)")}, {fieldname: "company", fieldtype: "Link", options: "Company", label: __("Company"), default: frm.doc.company, read_only:1}, + {fieldname: "currency", fieldtype: "Link", options: "Currency", label: __("Currency"), default: frm.doc.currency, read_only:1}, {fieldname: "grade", fieldtype: "Link", options: "Employee Grade", label: __("Employee Grade")}, {fieldname:'department', fieldtype:'Link', options: 'Department', label: __('Department')}, {fieldname:'designation', fieldtype:'Link', options: 'Designation', label: __('Designation')}, - {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, + {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, + {fieldname:"payroll_payable_account", fieldtype: "Link", options: "Account", filters: {"company": frm.doc.company, "root_type": "Liability", "is_group": 0, "account_currency": frm.doc.currency}, label: __("Payroll Payable Account")}, {fieldname:'base_variable', fieldtype:'Section Break'}, {fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1}, {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'}, diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json index 5f94929f0b..de56fc8457 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -13,6 +13,7 @@ "column_break1", "is_active", "payroll_frequency", + "currency", "is_default", "time_sheet_earning_detail", "salary_slip_based_on_timesheet", @@ -26,9 +27,9 @@ "deductions", "conditions_and_formula_variable_and_example", "net_pay_detail", - "column_break2", "total_earning", "total_deduction", + "column_break2", "net_pay", "account", "mode_of_payment", @@ -43,23 +44,17 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "letter_head", "fieldtype": "Link", "label": "Letter Head", - "options": "Letter Head", - "show_days": 1, - "show_seconds": 1 + "options": "Letter Head" }, { "fieldname": "column_break1", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -72,9 +67,7 @@ "oldfieldname": "is_active", "oldfieldtype": "Select", "options": "\nYes\nNo", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "default": "Monthly", @@ -82,9 +75,7 @@ "fieldname": "payroll_frequency", "fieldtype": "Select", "label": "Payroll Frequency", - "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily", - "show_days": 1, - "show_seconds": 1 + "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily" }, { "default": "No", @@ -95,62 +86,46 @@ "no_copy": 1, "options": "Yes\nNo", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "time_sheet_earning_detail", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "0", "fieldname": "salary_slip_based_on_timesheet", "fieldtype": "Check", - "label": "Salary Slip Based on Timesheet", - "show_days": 1, - "show_seconds": 1 + "label": "Salary Slip Based on Timesheet" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "description": "Salary Component for timesheet based payroll.", "fieldname": "salary_component", "fieldtype": "Link", "label": "Salary Component", - "options": "Salary Component", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Component" }, { "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "fieldname": "leave_encashment_amount_per_day", "fieldtype": "Currency", "label": "Leave Encashment Amount Per Day", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Amount)", - "options": "Company:company:default_currency", - "show_days": 1, - "show_seconds": 1 + "options": "currency" }, { "description": "Salary breakup based on Earning and Deduction.", @@ -158,9 +133,7 @@ "fieldtype": "Section Break", "oldfieldname": "earning_deduction", "oldfieldtype": "Section Break", - "precision": "2", - "show_days": 1, - "show_seconds": 1 + "precision": "2" }, { "fieldname": "earnings", @@ -168,9 +141,7 @@ "label": "Earnings", "oldfieldname": "earning_details", "oldfieldtype": "Table", - "options": "Salary Detail", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Detail" }, { "fieldname": "deductions", @@ -178,22 +149,16 @@ "label": "Deductions", "oldfieldname": "deduction_details", "oldfieldtype": "Table", - "options": "Salary Detail", - "show_days": 1, - "show_seconds": 1 + "options": "Salary Detail" }, { "fieldname": "net_pay_detail", "fieldtype": "Section Break", - "options": "Simple", - "show_days": 1, - "show_seconds": 1 + "options": "Simple" }, { "fieldname": "column_break2", "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -201,63 +166,45 @@ "fieldtype": "Currency", "hidden": 1, "label": "Total Earning", - "oldfieldname": "total_earning", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "total_deduction", "fieldtype": "Currency", "hidden": 1, "label": "Total Deduction", - "oldfieldname": "total_deduction", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "net_pay", "fieldtype": "Currency", "hidden": 1, "label": "Net Pay", - "options": "Company:company:default_currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "options": "currency", + "read_only": 1 }, { "fieldname": "account", "fieldtype": "Section Break", - "label": "Account", - "show_days": 1, - "show_seconds": 1 + "label": "Account" }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", - "options": "Mode of Payment", - "show_days": 1, - "show_seconds": 1 + "options": "Mode of Payment" }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "fieldname": "amended_from", @@ -266,23 +213,26 @@ "no_copy": 1, "options": "Salary Structure", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "conditions_and_formula_variable_and_example", "fieldtype": "HTML", - "label": "Conditions and Formula variable and example", - "show_days": 1, - "show_seconds": 1 + "label": "Conditions and Formula variable and example" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "reqd": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-06-22 17:07:26.129355", + "modified": "2020-09-30 11:30:32.190798", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure", diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index ffc16d73c2..877e41d93c 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.utils import flt, cint, cstr from frappe import _ @@ -88,24 +88,26 @@ class SalaryStructure(Document): return employees @frappe.whitelist() - def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None, - from_date=None, base=None, variable=None, income_tax_slab=None): - employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee) + def assign_salary_structure(self, grade=None, department=None, designation=None,employee=None, + payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): + employees = self.get_employees(company= self.company, grade= grade,department= department,designation= designation,name=employee) if employees: if len(employees) > 20: frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self,from_date=from_date, - base=base, variable=variable, income_tax_slab=income_tax_slab) + employees=employees, salary_structure=self, + payroll_payable_account=payroll_payable_account, + from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) else: - assign_salary_structure_for_employees(employees, self, from_date=from_date, - base=base, variable=variable, income_tax_slab=income_tax_slab) + assign_salary_structure_for_employees(employees, self, + payroll_payable_account=payroll_payable_account, + from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) else: frappe.msgprint(_("No Employee Found")) -def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None): +def assign_salary_structure_for_employees(employees, salary_structure, payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) count=0 @@ -115,7 +117,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date count +=1 salary_structures_assignment = create_salary_structures_assignment(employee, - salary_structure, from_date, base, variable, income_tax_slab) + salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab) salary_structures_assignments.append(salary_structures_assignment) frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) @@ -123,11 +125,22 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None): +def create_salary_structures_assignment(employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab=None): + if not payroll_payable_account: + payroll_payable_account = frappe.db.get_value('Company', salary_structure.company, 'default_payroll_payable_account') + if not payroll_payable_account: + frappe.throw(_('Please set "Default Payroll Payable Account" in Company Defaults')) + payroll_payable_account_currency = frappe.db.get_value('Account', payroll_payable_account, 'account_currency') + company_curency = erpnext.get_company_currency(salary_structure.company) + if payroll_payable_account_currency != salary_structure.currency and payroll_payable_account_currency != company_curency: + frappe.throw(_("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format(salary_structure.currency, company_curency)) + assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee assignment.salary_structure = salary_structure.name assignment.company = salary_structure.company + assignment.currency = salary_structure.currency + assignment.payroll_payable_account = payroll_payable_account assignment.from_date = from_date assignment.base = base assignment.variable = variable @@ -170,7 +183,8 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = "doctype": "Salary Slip", "field_map": { "total_earning": "gross_pay", - "name": "salary_structure" + "name": "salary_structure", + "currency": "currency" } } }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) @@ -188,7 +202,22 @@ def get_employees(salary_structure): filters={'salary_structure': salary_structure, 'docstatus': 1}, fields=['employee']) if not employees: - frappe.throw(_("There's no Employee with Salary Structure: {0}. \ - Assign {1} to an Employee to preview Salary Slip").format(salary_structure, salary_structure)) + frappe.throw(_("There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip").format( + salary_structure, salary_structure)) return list(set([d.employee for d in employees])) + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): + if len(filters) < 3: + return {} + + return frappe.db.sql(""" + select t1.salary_component + from `tabSalary Component` t1, `tabSalary Component Account` t2 + where t1.salary_component = t2.parent + and t1.type = %s + and t2.company = %s + order by salary_component + """, (filters['type'], filters['company']) ) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e04fda8120..abb669740b 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -94,7 +94,8 @@ class TestSalaryStructure(unittest.TestCase): self.assertFalse(("\n" in row.formula) or ("\n" in row.condition)) def test_salary_structures_assignment(self): - salary_structure = make_salary_structure("Salary Structure Sample", "Monthly") + company_currency = erpnext.get_default_currency() + salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", currency=company_currency) employee = "test_assign_stucture@salary.com" employee_doc_name = make_employee(employee) # clear the already assigned stuctures @@ -107,8 +108,13 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) + def test_multi_currency_salary_structure(self): + make_employee("test_muti_currency_employee@salary.com") + sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') + self.assertEqual(sal_struct.currency, 'USD') + def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None): + test_tax=False, company=None, currency=erpnext.get_default_currency()): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -120,7 +126,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account") + "payment_account": get_random("Account", filters={'account_currency': currency}), + "currency": currency } if other_details and isinstance(other_details, dict): details.update(other_details) @@ -134,16 +141,16 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company) + create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) payroll_period = create_payroll_period() - create_tax_slab(payroll_period, allow_tax_exemption=True) + create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee @@ -151,8 +158,15 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.variable = 5000 salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure + salary_structure_assignment.currency = currency + salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.submit() return salary_structure_assignment + +def get_payable_account(company=None): + if not company: + company = erpnext.get_default_company() + return frappe.db.get_value("Company", company, "default_payroll_payable_account") \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 818e853154..6cd897e95d 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -6,9 +6,6 @@ frappe.ui.form.on('Salary Structure Assignment', { frm.set_query("employee", function() { return { query: "erpnext.controllers.queries.employee_query", - filters: { - company: frm.doc.company - } } }); frm.set_query("salary_structure", function() { @@ -26,11 +23,25 @@ frappe.ui.form.on('Salary Structure Assignment', { filters: { company: frm.doc.company, docstatus: 1, - disabled: 0 + disabled: 0, + currency: frm.doc.currency + } + }; + }); + + frm.set_query("payroll_payable_account", function() { + var company_currency = erpnext.get_currency(frm.doc.company); + return { + filters: { + "company": frm.doc.company, + "root_type": "Liability", + "is_group": 0, + "account_currency": ["in", [frm.doc.currency, company_currency]], } } }); }, + employee: function(frm) { if(frm.doc.employee){ frappe.call({ @@ -52,5 +63,13 @@ frappe.ui.form.on('Salary Structure Assignment', { else{ frm.set_value("company", null); } + }, + + company: function(frm) { + if (frm.doc.company) { + frappe.db.get_value("Company", frm.doc.company, "default_payroll_payable_account", (r) => { + frm.set_value("payroll_payable_account", r.default_payroll_payable_account); + }); + } } }); diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index c84e034c72..92bb347661 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -11,11 +11,13 @@ "employee_name", "department", "company", + "payroll_payable_account", "column_break_6", "designation", "salary_structure", "from_date", "income_tax_slab", + "currency", "section_break_7", "base", "column_break_9", @@ -94,7 +96,7 @@ "fieldname": "base", "fieldtype": "Currency", "label": "Base", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "column_break_9", @@ -104,7 +106,7 @@ "fieldname": "variable", "fieldtype": "Currency", "label": "Variable", - "options": "Company:company:default_currency" + "options": "currency" }, { "fieldname": "amended_from", @@ -116,15 +118,35 @@ "read_only": 1 }, { + "depends_on": "salary_structure", "fieldname": "income_tax_slab", "fieldtype": "Link", "label": "Income Tax Slab", "options": "Income Tax Slab" + }, + { + "default": "Company:company:default_currency", + "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", + "fetch_from": "salary_structure.currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "depends_on": "employee", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "options": "Account" } ], "is_submittable": 1, "links": [], - "modified": "2020-06-22 19:58:09.964692", + "modified": "2020-11-30 18:07:48.251311", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index 668e0ec471..dccb5df1a1 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -13,6 +13,8 @@ class DuplicateAssignment(frappe.ValidationError): pass class SalaryStructureAssignment(Document): def validate(self): self.validate_dates() + self.validate_income_tax_slab() + self.set_payroll_payable_account() def validate_dates(self): joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, @@ -31,6 +33,24 @@ class SalaryStructureAssignment(Document): frappe.throw(_("From Date {0} cannot be after employee's relieving Date {1}") .format(self.from_date, relieving_date)) + def validate_income_tax_slab(self): + if not self.income_tax_slab: + return + + income_tax_slab_currency = frappe.db.get_value('Income Tax Slab', self.income_tax_slab, 'currency') + if self.currency != income_tax_slab_currency: + frappe.throw(_("Currency of selected Income Tax Slab should be {0} instead of {1}").format(self.currency, income_tax_slab_currency)) + + def set_payroll_payable_account(self): + if not self.payroll_payable_account: + payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') + if not payroll_payable_account: + payroll_payable_account = frappe.db.get_value( + "Account", { + "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( + "Company", self.company, "default_currency"), "is_group": 0}) + self.payroll_payable_account = payroll_payable_account + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: return None @@ -43,3 +63,10 @@ def get_assigned_salary_structure(employee, on_date): 'on_date': on_date, }) return salary_structure[0][0] if salary_structure else None + +@frappe.whitelist() +def get_employee_currency(employee): + employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') + if not employee_currency: + frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(employee)) + return employee_currency \ No newline at end of file diff --git a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json index 94eda4c043..65d3824f3a 100644 --- a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json +++ b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json @@ -19,13 +19,15 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "From Amount", + "options": "currency", "reqd": 1 }, { "fieldname": "to_amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "To Amount" + "label": "To Amount", + "options": "currency" }, { "default": "0", @@ -53,7 +55,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-22 18:16:07.596493", + "modified": "2020-10-19 13:44:39.549337", "modified_by": "Administrator", "module": "Payroll", "name": "Taxable Salary Slab", diff --git a/erpnext/payroll/report/salary_register/salary_register.js b/erpnext/payroll/report/salary_register/salary_register.js index 885e3d13c7..eb4acb91a7 100644 --- a/erpnext/payroll/report/salary_register/salary_register.js +++ b/erpnext/payroll/report/salary_register/salary_register.js @@ -8,34 +8,48 @@ frappe.query_reports["Salary Register"] = { "label": __("From"), "fieldtype": "Date", "default": frappe.datetime.add_months(frappe.datetime.get_today(),-1), - "reqd": 1 + "reqd": 1, + "width": "100px" }, { "fieldname":"to_date", "label": __("To"), "fieldtype": "Date", "default": frappe.datetime.get_today(), - "reqd": 1 + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "options": "Currency", + "label": __("Currency"), + "default": erpnext.get_currency(frappe.defaults.get_default("Company")), + "width": "50px" }, { "fieldname":"employee", "label": __("Employee"), "fieldtype": "Link", - "options": "Employee" + "options": "Employee", + "width": "100px" }, { "fieldname":"company", "label": __("Company"), "fieldtype": "Link", "options": "Company", - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "width": "100px", + "reqd": 1 }, { "fieldname":"docstatus", "label":__("Document Status"), "fieldtype":"Select", "options":["Draft", "Submitted", "Cancelled"], - "default":"Submitted" + "default": "Submitted", + "width": "100px" } ] } diff --git a/erpnext/payroll/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py index 87010855fd..a1b1a8c56b 100644 --- a/erpnext/payroll/report/salary_register/salary_register.py +++ b/erpnext/payroll/report/salary_register/salary_register.py @@ -2,18 +2,22 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe.utils import flt from frappe import _ def execute(filters=None): if not filters: filters = {} - salary_slips = get_salary_slips(filters) + currency = None + if filters.get('currency'): + currency = filters.get('currency') + company_currency = erpnext.get_company_currency(filters.get("company")) + salary_slips = get_salary_slips(filters, company_currency) if not salary_slips: return [], [] columns, earning_types, ded_types = get_columns(salary_slips) - ss_earning_map = get_ss_earning_map(salary_slips) - ss_ded_map = get_ss_ded_map(salary_slips) + ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) + ss_ded_map = get_ss_ded_map(salary_slips,currency, company_currency) doj_map = get_employee_doj_map() data = [] @@ -21,24 +25,30 @@ def execute(filters=None): row = [ss.name, ss.employee, ss.employee_name, doj_map.get(ss.employee), ss.branch, ss.department, ss.designation, ss.company, ss.start_date, ss.end_date, ss.leave_without_pay, ss.payment_days] - if not ss.branch == None:columns[3] = columns[3].replace('-1','120') - if not ss.department == None: columns[4] = columns[4].replace('-1','120') - if not ss.designation == None: columns[5] = columns[5].replace('-1','120') - if not ss.leave_without_pay == None: columns[9] = columns[9].replace('-1','130') + if ss.branch is not None: columns[3] = columns[3].replace('-1','120') + if ss.department is not None: columns[4] = columns[4].replace('-1','120') + if ss.designation is not None: columns[5] = columns[5].replace('-1','120') + if ss.leave_without_pay is not None: columns[9] = columns[9].replace('-1','130') for e in earning_types: row.append(ss_earning_map.get(ss.name, {}).get(e)) - row += [ss.gross_pay] + if currency == company_currency: + row += [flt(ss.gross_pay) * flt(ss.exchange_rate)] + else: + row += [ss.gross_pay] for d in ded_types: row.append(ss_ded_map.get(ss.name, {}).get(d)) row.append(ss.total_loan_repayment) - row += [ss.total_deduction, ss.net_pay] - + if currency == company_currency: + row += [flt(ss.total_deduction) * flt(ss.exchange_rate), flt(ss.net_pay) * flt(ss.exchange_rate)] + else: + row += [ss.total_deduction, ss.net_pay] + row.append(currency or company_currency) data.append(row) return columns, data @@ -46,10 +56,19 @@ def execute(filters=None): def get_columns(salary_slips): """ columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150",_("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", - _("Date of Joining") + "::80", _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", - _("End Date") + "::80", _("Leave Without Pay") + ":Float:130", _("Payment Days") + ":Float:120" + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:130", + _("Payment Days") + ":Float:120", + _("Currency") + ":Link/Currency:80" ] """ columns = [ @@ -73,15 +92,15 @@ def get_columns(salary_slips): return columns, salary_components[_("Earning")], salary_components[_("Deduction")] -def get_salary_slips(filters): +def get_salary_slips(filters, company_currency): filters.update({"from_date": filters.get("from_date"), "to_date":filters.get("to_date")}) - conditions, filters = get_conditions(filters) + conditions, filters = get_conditions(filters, company_currency) salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s order by employee""" % conditions, filters, as_dict=1) return salary_slips or [] -def get_conditions(filters): +def get_conditions(filters, company_currency): conditions = "" doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} @@ -92,6 +111,8 @@ def get_conditions(filters): if filters.get("to_date"): conditions += " and end_date <= %(to_date)s" if filters.get("company"): conditions += " and company = %(company)s" if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("currency") and filters.get("currency") != company_currency: + conditions += " and currency = %(currency)s" return conditions, filters @@ -103,26 +124,32 @@ def get_employee_doj_map(): FROM `tabEmployee` """)) -def get_ss_earning_map(salary_slips): - ss_earnings = frappe.db.sql("""select parent, salary_component, amount - from `tabSalary Detail` where parent in (%s)""" % +def get_ss_earning_map(salary_slips, currency, company_currency): + ss_earnings = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) ss_earning_map = {} for d in ss_earnings: ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) - ss_earning_map[d.parent][d.salary_component] = flt(d.amount) + if currency == company_currency: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + else: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) return ss_earning_map -def get_ss_ded_map(salary_slips): - ss_deductions = frappe.db.sql("""select parent, salary_component, amount - from `tabSalary Detail` where parent in (%s)""" % +def get_ss_ded_map(salary_slips, currency, company_currency): + ss_deductions = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) ss_ded_map = {} for d in ss_deductions: ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) - ss_ded_map[d.parent][d.salary_component] = flt(d.amount) + if currency == company_currency: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + else: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) return ss_ded_map From ad57eef40c1dc6710218387a9d1f5c8ead43c7f3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 1 Dec 2020 09:14:57 +0530 Subject: [PATCH 211/229] fix(product-listing): Check if customer exists (#24030) - It might happen that perty_name might not always be Customer (it might be Supplier as well) --- erpnext/shopping_cart/cart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 0ccc0252c3..c2549fe7dd 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -345,7 +345,7 @@ def _set_price_list(cart_settings, quotation=None): selling_price_list = None # check if default customer price list exists - if party_name: + if party_name and frappe.db.exists("Customer", party_name): selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) # check default price list in shopping cart From 029b9c08ddf6c4b2b77b3c8f221e16bc3af76fa0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 1 Dec 2020 09:33:17 +0530 Subject: [PATCH 212/229] Update bom.py --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c6699200dc..6363242b0a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -169,7 +169,7 @@ class BOM(WebsiteGenerator): 'qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), - 'include_item_in_manufacturing': cint(args['transfer_for_manufacture'], 0), + 'include_item_in_manufacturing': cint(args.get('transfer_for_manufacture')), 'sourced_by_supplier' : args.get('sourced_by_supplier', 0) } From a3845a95ed8c9bf7fe3d0ac977324a01f2d92dc2 Mon Sep 17 00:00:00 2001 From: Leela vadlamudi Date: Tue, 1 Dec 2020 13:04:53 +0530 Subject: [PATCH 213/229] feat: Introducing telephony module (#24032) --- .editorconfig | 14 +++ erpnext/hooks.py | 4 +- erpnext/modules.txt | 3 +- erpnext/public/build.json | 3 +- erpnext/public/js/call_popup/call_popup.js | 2 +- erpnext/public/js/telephony.js | 23 ++++ .../call_log => telephony}/__init__.py | 0 erpnext/telephony/doctype/__init__.py | 0 .../telephony/doctype/call_log/__init__.py | 0 .../telephony/doctype/call_log/call_log.js | 8 ++ .../doctype/call_log/call_log.json | 5 +- .../doctype/call_log/call_log.py | 0 .../doctype/call_log/test_call_log.py | 10 ++ .../__init__.py | 0 .../incoming_call_handling_schedule.json | 60 +++++++++++ .../incoming_call_handling_schedule.py | 10 ++ .../incoming_call_settings/__init__.py | 0 .../incoming_call_settings.js | 102 ++++++++++++++++++ .../incoming_call_settings.json | 82 ++++++++++++++ .../incoming_call_settings.py | 63 +++++++++++ .../test_incoming_call_settings.py | 10 ++ 21 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 .editorconfig create mode 100644 erpnext/public/js/telephony.js rename erpnext/{communication/doctype/call_log => telephony}/__init__.py (100%) create mode 100644 erpnext/telephony/doctype/__init__.py create mode 100644 erpnext/telephony/doctype/call_log/__init__.py create mode 100644 erpnext/telephony/doctype/call_log/call_log.js rename erpnext/{communication => telephony}/doctype/call_log/call_log.json (97%) rename erpnext/{communication => telephony}/doctype/call_log/call_log.py (100%) create mode 100644 erpnext/telephony/doctype/call_log/test_call_log.py create mode 100644 erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py create mode 100644 erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json create mode 100644 erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py create mode 100644 erpnext/telephony/doctype/incoming_call_settings/__init__.py create mode 100644 erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js create mode 100644 erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json create mode 100644 erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py create mode 100644 erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..24f122a8d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js}] +indent_style = tab +indent_size = 4 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 726ab6e22a..987345697a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -271,11 +271,11 @@ doc_events = { }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", - "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", + "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information", "validate": "erpnext.crm.utils.update_lead_phone_numbers" }, "Lead": { - "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" + "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information" }, "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 1e2aeea36a..62f5dce846 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -25,4 +25,5 @@ Hub Node Quality Management Communication Loan Management -Payroll \ No newline at end of file +Payroll +Telephony \ No newline at end of file diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2695502269..2f15cbcef1 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -49,7 +49,8 @@ "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", "public/js/call_popup/call_popup.js", - "public/js/utils/dimension_tree_filter.js" + "public/js/utils/dimension_tree_filter.js", + "public/js/telephony.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js index 5e4d4a585f..aeb3b387f2 100644 --- a/erpnext/public/js/call_popup/call_popup.js +++ b/erpnext/public/js/call_popup/call_popup.js @@ -74,7 +74,7 @@ class CallPopup { 'click': () => { const call_summary = this.dialog.get_value('call_summary'); if (!call_summary) return; - frappe.xcall('erpnext.communication.doctype.call_log.call_log.add_call_summary', { + frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', { 'call_log': this.call_log.name, 'summary': call_summary, }).then(() => { diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js new file mode 100644 index 0000000000..bd7f890306 --- /dev/null +++ b/erpnext/public/js/telephony.js @@ -0,0 +1,23 @@ +frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { + make_input() { + this._super(); + if (this.df.options == 'Phone') { + this.setup_phone(); + } + }, + setup_phone() { + if (frappe.phone_call.handler) { + this.$wrapper.find('.control-input') + .append(` + + + + + `) + .find('.phone-btn') + .click(() => { + frappe.phone_call.handler(this.get_value(), this.frm); + }); + } + } +}); \ No newline at end of file diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/telephony/__init__.py similarity index 100% rename from erpnext/communication/doctype/call_log/__init__.py rename to erpnext/telephony/__init__.py diff --git a/erpnext/telephony/doctype/__init__.py b/erpnext/telephony/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/call_log/__init__.py b/erpnext/telephony/doctype/call_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/call_log/call_log.js b/erpnext/telephony/doctype/call_log/call_log.js new file mode 100644 index 0000000000..977f86da0d --- /dev/null +++ b/erpnext/telephony/doctype/call_log/call_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Call Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json similarity index 97% rename from erpnext/communication/doctype/call_log/call_log.json rename to erpnext/telephony/doctype/call_log/call_log.json index 31e79f17cd..55ad2baefd 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/telephony/doctype/call_log/call_log.json @@ -137,12 +137,11 @@ "read_only": 1 } ], - "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-25 17:08:34.085731", + "modified": "2020-11-25 14:32:44.407815", "modified_by": "Administrator", - "module": "Communication", + "module": "Telephony", "name": "Call Log", "owner": "Administrator", "permissions": [ diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py similarity index 100% rename from erpnext/communication/doctype/call_log/call_log.py rename to erpnext/telephony/doctype/call_log/call_log.py diff --git a/erpnext/telephony/doctype/call_log/test_call_log.py b/erpnext/telephony/doctype/call_log/test_call_log.py new file mode 100644 index 0000000000..faa63041ba --- /dev/null +++ b/erpnext/telephony/doctype/call_log/test_call_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestCallLog(unittest.TestCase): + pass diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json new file mode 100644 index 0000000000..6d46b4e2cd --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "creation": "2020-11-19 11:15:54.967710", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day_of_week", + "from_time", + "to_time", + "agent_group" + ], + "fields": [ + { + "fieldname": "day_of_week", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day Of Week", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + }, + { + "default": "9:00:00", + "fieldname": "from_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "default": "17:00:00", + "fieldname": "to_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "agent_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Agent Group", + "options": "Employee Group", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-19 11:15:54.967710", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Incoming Call Handling Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py new file mode 100644 index 0000000000..fcf29745e2 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_handling_schedule/incoming_call_handling_schedule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class IncomingCallHandlingSchedule(Document): + pass diff --git a/erpnext/telephony/doctype/incoming_call_settings/__init__.py b/erpnext/telephony/doctype/incoming_call_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js new file mode 100644 index 0000000000..1bcc846132 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.js @@ -0,0 +1,102 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +function time_to_seconds(time_str) { + // Convert time string of format HH:MM:SS into seconds. + let seq = time_str.split(':'); + seq = seq.map((n) => parseInt(n)); + return (seq[0]*60*60) + (seq[1]*60) + seq[2]; +} + +function number_sort(array, ascending=true) { + let array_copy = [...array]; + if (ascending) { + array_copy.sort((a, b) => a-b); // ascending order + } else { + array_copy.sort((a, b) => b-a); // descending order + } + return array_copy; +} + +function groupby(items, key) { + // Group the list of items using the given key. + const obj = {}; + items.forEach((item) => { + if (item[key] in obj) { + obj[item[key]].push(item); + } else { + obj[item[key]] = [item]; + } + }); + return obj; +} + +function check_timeslot_overlap(ts1, ts2) { + /// Timeslot is a an array of length 2 ex: [from_time, to_time] + /// time in timeslot is an integer represents number of seconds. + if ((ts1[0] < ts2[0] && ts1[1] <= ts2[0]) || (ts1[0] >= ts2[1] && ts1[1] > ts2[1])) { + return false; + } + return true; +} + +function validate_call_schedule(schedule) { + validate_call_schedule_timeslot(schedule); + validate_call_schedule_overlaps(schedule); +} + +function validate_call_schedule_timeslot(schedule) { + // Make sure that to time slot is ahead of from time slot. + let errors = []; + + for (let row in schedule) { + let record = schedule[row]; + let from_time_in_secs = time_to_seconds(record.from_time); + let to_time_in_secs = time_to_seconds(record.to_time); + if (from_time_in_secs >= to_time_in_secs) { + errors.push(__('Call Schedule Row {0}: To time slot should always be ahead of From time slot.', [row])); + } + } + + if (errors.length > 0) { + frappe.throw(errors.join("
")); + } +} + +function is_call_schedule_overlapped(day_schedule) { + // Check if any time slots are overlapped in a day schedule. + let timeslots = []; + day_schedule.forEach((record)=> { + timeslots.push([time_to_seconds(record.from_time), time_to_seconds(record.to_time)]); + }); + + if (timeslots.length < 2) { + return false; + } + + timeslots = number_sort(timeslots); + + // Sorted timeslots will be in ascending order if not overlapped. + for (let i=1; i < timeslots.length; i++) { + if (check_timeslot_overlap(timeslots[i-1], timeslots[i])) { + return true; + } + } + return false; +} + +function validate_call_schedule_overlaps(schedule) { + let group_by_day = groupby(schedule, 'day_of_week'); + for (const [day, day_schedule] of Object.entries(group_by_day)) { + if (is_call_schedule_overlapped(day_schedule)) { + frappe.throw(__('Please fix overlapping time slots for {0}', [day])); + } + } +} + +frappe.ui.form.on('Incoming Call Settings', { + validate(frm) { + validate_call_schedule(frm.doc.call_handling_schedule); + } +}); + diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json new file mode 100644 index 0000000000..3ffb3e49db --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-11-19 10:37:20.734245", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "call_routing", + "column_break_2", + "greeting_message", + "agent_busy_message", + "agent_unavailable_message", + "section_break_6", + "call_handling_schedule" + ], + "fields": [ + { + "default": "Sequential", + "fieldname": "call_routing", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Call Routing", + "options": "Sequential\nSimultaneous" + }, + { + "fieldname": "greeting_message", + "fieldtype": "Data", + "label": "Greeting Message" + }, + { + "fieldname": "agent_busy_message", + "fieldtype": "Data", + "label": "Agent Busy Message" + }, + { + "fieldname": "agent_unavailable_message", + "fieldtype": "Data", + "label": "Agent Unavailable Message" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "call_handling_schedule", + "fieldtype": "Table", + "label": "Call Handling Schedule", + "options": "Incoming Call Handling Schedule", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-19 11:17:14.527862", + "modified_by": "Administrator", + "module": "Telephony", + "name": "Incoming Call Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py new file mode 100644 index 0000000000..2b2008a8ab --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from datetime import datetime +from typing import Tuple +from frappe import _ + +class IncomingCallSettings(Document): + def validate(self): + """List of validations + * Make sure that to time slot is ahead of from time slot in call schedule + * Make sure that no overlapping timeslots for a given day + """ + self.validate_call_schedule_timeslot(self.call_handling_schedule) + self.validate_call_schedule_overlaps(self.call_handling_schedule) + + def validate_call_schedule_timeslot(self, schedule: list): + """ Make sure that to time slot is ahead of from time slot. + """ + errors = [] + for record in schedule: + from_time = self.time_to_seconds(record.from_time) + to_time = self.time_to_seconds(record.to_time) + if from_time >= to_time: + errors.append( + _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx) + ) + + if errors: + frappe.throw('
'.join(errors)) + + def validate_call_schedule_overlaps(self, schedule: list): + """Check if any time slots are overlapped in a day schedule. + """ + week_days = set([each.day_of_week for each in schedule]) + + for day in week_days: + timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day] + + # convert time in timeslot into an integer represents number of seconds + timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots)) + if len(timeslots) < 2: continue + + for i in range(1, len(timeslots)): + if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]): + frappe.throw(_('Please fix overlapping time slots for {0}.').format(day)) + + @staticmethod + def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool: + if (ts1[0] < ts2[0] and ts1[1] <= ts2[0]) or (ts1[0] >= ts2[1] and ts1[1] > ts2[1]): + return False + return True + + @staticmethod + def time_to_seconds(time: str) -> int: + """Convert time string of format HH:MM:SS into seconds + """ + date_time = datetime.strptime(time, "%H:%M:%S") + return date_time - datetime(1900, 1, 1) diff --git a/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py new file mode 100644 index 0000000000..c058c117b3 --- /dev/null +++ b/erpnext/telephony/doctype/incoming_call_settings/test_incoming_call_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestIncomingCallSettings(unittest.TestCase): + pass From 1ac040a418a4dfd541d7b3a9c46956a043ca276e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 1 Dec 2020 13:50:01 +0530 Subject: [PATCH 214/229] fix: GSTR report --- erpnext/regional/report/gstr_1/gstr_1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 282efe4790..837929709e 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -78,7 +78,7 @@ class Gstr1Report(object): place_of_supply = invoice_details.get("place_of_supply") ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{ + b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin, inv),{ "place_of_supply": "", "ecommerce_gstin": "", "rate": "", @@ -90,7 +90,7 @@ class Gstr1Report(object): "invoice_value": invoice_details.get("base_grand_total"), }) - row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) + row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin, inv)) row["place_of_supply"] = place_of_supply row["ecommerce_gstin"] = ecommerce_gstin row["rate"] = rate From 1c9410e5e835ca81ffc4507a1bb0b3f4e0a801a1 Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Mon, 13 Jul 2020 16:25:09 +0800 Subject: [PATCH 215/229] feat(shipment): Shipment Doctype with Integrations --- .../doctype/letmeship/__init__.py | 0 .../doctype/letmeship/letmeship.js | 8 + .../doctype/letmeship/letmeship.json | 55 ++ .../doctype/letmeship/letmeship.py | 396 +++++++++ .../doctype/letmeship/test_letmeship.py | 10 + .../doctype/packlink/__init__.py | 0 .../doctype/packlink/packlink.js | 8 + .../doctype/packlink/packlink.json | 48 ++ .../doctype/packlink/packlink.py | 237 ++++++ .../doctype/packlink/test_packlink.py | 10 + .../doctype/sendcloud/__init__.py | 0 .../doctype/sendcloud/sendcloud.js | 8 + .../doctype/sendcloud/sendcloud.json | 56 ++ .../doctype/sendcloud/sendcloud.py | 171 ++++ .../doctype/sendcloud/test_sendcloud.py | 10 + erpnext/erpnext_integrations/utils.py | 12 +- .../doctype/delivery_note/delivery_note.js | 12 + .../doctype/delivery_note/delivery_note.py | 53 ++ .../stock/doctype/parcel_service/__init__.py | 0 .../doctype/parcel_service/parcel_service.js | 8 + .../parcel_service/parcel_service.json | 56 ++ .../doctype/parcel_service/parcel_service.py | 10 + .../parcel_service/test_parcel_service.py | 10 + .../doctype/parcel_service_type/__init__.py | 0 .../parcel_service_type.js | 12 + .../parcel_service_type.json | 89 ++ .../parcel_service_type.py | 22 + .../test_parcel_service_type.py | 10 + .../parcel_service_type_alias/__init__.py | 0 .../parcel_service_type_alias.json | 41 + .../parcel_service_type_alias.py | 10 + erpnext/stock/doctype/shipment/__init__.py | 0 erpnext/stock/doctype/shipment/api/utils.py | 67 ++ erpnext/stock/doctype/shipment/shipment.js | 772 ++++++++++++++++++ erpnext/stock/doctype/shipment/shipment.json | 478 +++++++++++ erpnext/stock/doctype/shipment/shipment.py | 300 +++++++ .../stock/doctype/shipment/shipment_list.js | 8 + .../shipment/shipment_service_selector.html | 70 ++ .../stock/doctype/shipment/test_shipment.py | 333 ++++++++ .../shipment_delivery_notes/__init__.py | 0 .../shipment_delivery_notes.json | 41 + .../shipment_delivery_notes.py | 10 + .../__init__.py | 0 .../shipment_notification_subscriptions.json | 40 + .../shipment_notification_subscriptions.py | 10 + .../stock/doctype/shipment_parcel/__init__.py | 0 .../shipment_parcel/shipment_parcel.json | 65 ++ .../shipment_parcel/shipment_parcel.py | 10 + .../shipment_parcel_template/__init__.py | 0 .../shipment_parcel_template.js | 8 + .../shipment_parcel_template.json | 78 ++ .../shipment_parcel_template.py | 10 + .../test_shipment_parcel_template.py | 10 + .../__init__.py | 0 .../shipment_status_update_subscriptions.json | 40 + .../shipment_status_update_subscriptions.py | 10 + 56 files changed, 3721 insertions(+), 1 deletion(-) create mode 100644 erpnext/erpnext_integrations/doctype/letmeship/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/letmeship/letmeship.js create mode 100644 erpnext/erpnext_integrations/doctype/letmeship/letmeship.json create mode 100644 erpnext/erpnext_integrations/doctype/letmeship/letmeship.py create mode 100644 erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py create mode 100644 erpnext/erpnext_integrations/doctype/packlink/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/packlink/packlink.js create mode 100644 erpnext/erpnext_integrations/doctype/packlink/packlink.json create mode 100644 erpnext/erpnext_integrations/doctype/packlink/packlink.py create mode 100644 erpnext/erpnext_integrations/doctype/packlink/test_packlink.py create mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js create mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json create mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py create mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py create mode 100644 erpnext/stock/doctype/parcel_service/__init__.py create mode 100644 erpnext/stock/doctype/parcel_service/parcel_service.js create mode 100644 erpnext/stock/doctype/parcel_service/parcel_service.json create mode 100644 erpnext/stock/doctype/parcel_service/parcel_service.py create mode 100644 erpnext/stock/doctype/parcel_service/test_parcel_service.py create mode 100644 erpnext/stock/doctype/parcel_service_type/__init__.py create mode 100644 erpnext/stock/doctype/parcel_service_type/parcel_service_type.js create mode 100644 erpnext/stock/doctype/parcel_service_type/parcel_service_type.json create mode 100644 erpnext/stock/doctype/parcel_service_type/parcel_service_type.py create mode 100644 erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py create mode 100644 erpnext/stock/doctype/parcel_service_type_alias/__init__.py create mode 100644 erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json create mode 100644 erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py create mode 100644 erpnext/stock/doctype/shipment/__init__.py create mode 100644 erpnext/stock/doctype/shipment/api/utils.py create mode 100644 erpnext/stock/doctype/shipment/shipment.js create mode 100644 erpnext/stock/doctype/shipment/shipment.json create mode 100644 erpnext/stock/doctype/shipment/shipment.py create mode 100644 erpnext/stock/doctype/shipment/shipment_list.js create mode 100644 erpnext/stock/doctype/shipment/shipment_service_selector.html create mode 100644 erpnext/stock/doctype/shipment/test_shipment.py create mode 100644 erpnext/stock/doctype/shipment_delivery_notes/__init__.py create mode 100644 erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json create mode 100644 erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py create mode 100644 erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py create mode 100644 erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json create mode 100644 erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py create mode 100644 erpnext/stock/doctype/shipment_parcel/__init__.py create mode 100644 erpnext/stock/doctype/shipment_parcel/shipment_parcel.json create mode 100644 erpnext/stock/doctype/shipment_parcel/shipment_parcel.py create mode 100644 erpnext/stock/doctype/shipment_parcel_template/__init__.py create mode 100644 erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js create mode 100644 erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json create mode 100644 erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py create mode 100644 erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py create mode 100644 erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py create mode 100644 erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json create mode 100644 erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py diff --git a/erpnext/erpnext_integrations/doctype/letmeship/__init__.py b/erpnext/erpnext_integrations/doctype/letmeship/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js new file mode 100644 index 0000000000..1e5e372dff --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('LetMeShip', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json new file mode 100644 index 0000000000..4a9a70f251 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2020-07-23 10:55:19.669830", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "api_id", + "api_password" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "api_id", + "fieldtype": "Data", + "label": "API ID", + "read_only_depends_on": "eval:doc.enabled == 0" + }, + { + "fieldname": "api_password", + "fieldtype": "Data", + "label": "API Password", + "read_only_depends_on": "eval:doc.enabled == 0" + } + ], + "issingle": 1, + "links": [], + "modified": "2020-08-05 16:33:44.548230", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "LetMeShip", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py new file mode 100644 index 0000000000..3ad06dbb58 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py @@ -0,0 +1,396 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import requests +import frappe +import json +import re +from frappe import _ +from frappe.model.document import Document +from erpnext.erpnext_integrations.utils import get_tracking_url + +LETMESHIP_PROVIDER = 'LetMeShip' + +class LetMeShip(Document): + pass + +def get_letmeship_available_services(delivery_to_type, pickup_address, + delivery_address, shipment_parcel, description_of_content, pickup_date, + value_of_goods, pickup_contact=None, delivery_contact=None): + # Retrieve rates at LetMeShip from specification stated. + enabled = frappe.db.get_single_value('LetMeShip','enabled') + api_id = frappe.db.get_single_value('LetMeShip','api_id') + api_password = frappe.db.get_single_value('LetMeShip','api_password') + if not enabled or not api_id or not api_password: + return [] + + set_letmeship_specific_fields(pickup_contact, delivery_contact) + + # LetMeShip have limit of 30 characters for Company field + if len(pickup_address.address_title) > 30: + pickup_address.address_title = pickup_address.address_title[:30] + if len(delivery_address.address_title) > 30: + delivery_address.address_title = delivery_address.address_title[:30] + parcel_list = get_parcel_list(json.loads(shipment_parcel), description_of_content) + + url = 'https://api.letmeship.com/v1/available' + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Access-Control-Allow-Origin': 'string' + } + payload = {'pickupInfo': { + 'address': { + 'countryCode': pickup_address.country_code, + 'zip': pickup_address.pincode, + 'city': pickup_address.city, + 'street': pickup_address.address_line1, + 'addressInfo1': pickup_address.address_line2, + 'houseNo': '', + }, + 'company': pickup_address.address_title, + 'person': { + 'title': pickup_contact.title, + 'firstname': pickup_contact.first_name, + 'lastname': pickup_contact.last_name + }, + 'phone': { + 'phoneNumber': pickup_contact.phone, + 'phoneNumberPrefix': pickup_contact.phone_prefix + }, + 'email': pickup_contact.email, + }, 'deliveryInfo': { + 'address': { + 'countryCode': delivery_address.country_code, + 'zip': delivery_address.pincode, + 'city': delivery_address.city, + 'street': delivery_address.address_line1, + 'addressInfo1': delivery_address.address_line2, + 'houseNo': '', + }, + 'company': delivery_address.address_title, + 'person': { + 'title': delivery_contact.title, + 'firstname': delivery_contact.first_name, + 'lastname': delivery_contact.last_name + }, + 'phone': { + 'phoneNumber': delivery_contact.phone, + 'phoneNumberPrefix': delivery_contact.phone_prefix + }, + 'email': delivery_contact.email, + }, 'shipmentDetails': { + 'contentDescription': description_of_content, + 'shipmentType': 'PARCEL', + 'shipmentSettings': { + 'saturdayDelivery': False, + 'ddp': False, + 'insurance': False, + 'pickupOrder': False, + 'pickupTailLift': False, + 'deliveryTailLift': False, + 'holidayDelivery': False, + }, + 'goodsValue': value_of_goods, + 'parcelList': parcel_list, + 'pickupInterval': {'date': pickup_date}, + }} + try: + available_services = [] + response_data = requests.post( + url=url, + auth=(api_id, api_password), + headers=headers, + data=json.dumps(payload) + ) + response_data = json.loads(response_data.text) + if 'serviceList' in response_data: + for response in response_data['serviceList']: + available_service = frappe._dict() + basic_info = response['baseServiceDetails'] + price_info = basic_info['priceInfo'] + available_service.service_provider = LETMESHIP_PROVIDER + available_service.id = basic_info['id'] + available_service.carrier = basic_info['carrier'] + available_service.carrier_name = basic_info['name'] + available_service.service_name = '' + available_service.is_preferred = 0 + available_service.real_weight = price_info['realWeight'] + available_service.total_price = price_info['netPrice'] + available_service.price_info = price_info + available_services.append(available_service) + return available_services + else: + frappe.throw( + _('Error occurred while fetching LetMeShip prices: {0}') + .format(response_data['message']) + ) + except Exception as exc: + frappe.msgprint( + _('Error occurred while fetching LetMeShip Prices: {0}') + .format(str(exc)), + indicator='orange', + alert=True + ) + return [] + + +def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, description_of_content, + pickup_date, value_of_goods, service_info, shipment_notific_email, tracking_notific_email, + pickup_contact=None, delivery_contact=None): + # Create a transaction at LetMeShip + # LetMeShip have limit of 30 characters for Company field + enabled = frappe.db.get_single_value('LetMeShip','enabled') + api_id = frappe.db.get_single_value('LetMeShip','api_id') + api_password = frappe.db.get_single_value('LetMeShip','api_password') + if not enabled or not api_id or not api_password: + return [] + + set_letmeship_specific_fields(pickup_contact, delivery_contact) + + if len(pickup_address.address_title) > 30: + pickup_address.address_title = pickup_address.address_title[:30] + if len(delivery_address.address_title) > 30: + delivery_address.address_title = delivery_address.address_title[:30] + + parcel_list = get_parcel_list(json.loads(shipment_parcel), description_of_content) + url = 'https://api.letmeship.com/v1/shipments' + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Access-Control-Allow-Origin': 'string' + } + payload = { + 'pickupInfo': { + 'address': { + 'countryCode': pickup_address.country_code, + 'zip': pickup_address.pincode, + 'city': pickup_address.city, + 'street': pickup_address.address_line1, + 'addressInfo1': pickup_address.address_line2, + 'houseNo': '', + }, + 'company': pickup_address.address_title, + 'person': { + 'title': pickup_contact.title, + 'firstname': pickup_contact.first_name, + 'lastname': pickup_contact.last_name + }, + 'phone': { + 'phoneNumber': pickup_contact.phone, + 'phoneNumberPrefix': pickup_contact.phone_prefix + }, + 'email': pickup_contact.email, + }, + 'deliveryInfo': { + 'address': { + 'countryCode': delivery_address.country_code, + 'zip': delivery_address.pincode, + 'city': delivery_address.city, + 'street': delivery_address.address_line1, + 'addressInfo1': delivery_address.address_line2, + 'houseNo': '', + }, + 'company': delivery_address.address_title, + 'person': { + 'title': delivery_contact.title, + 'firstname': delivery_contact.first_name, + 'lastname': delivery_contact.last_name + }, + 'phone': { + 'phoneNumber': delivery_contact.phone, + 'phoneNumberPrefix': delivery_contact.phone_prefix + }, + 'email': delivery_contact.email, + }, + 'service': { + 'baseServiceDetails': { + 'id': service_info['id'], + 'name': service_info['service_name'], + 'carrier': service_info['carrier'], + 'priceInfo': service_info['price_info'], + }, + 'supportedExWorkType': [], + 'messages': [''], + 'description': '', + 'serviceInfo': '', + }, + 'shipmentDetails': { + 'contentDescription': description_of_content, + 'shipmentType': 'PARCEL', + 'shipmentSettings': { + 'saturdayDelivery': False, + 'ddp': False, + 'insurance': False, + 'pickupOrder': False, + 'pickupTailLift': False, + 'deliveryTailLift': False, + 'holidayDelivery': False, + }, + 'goodsValue': value_of_goods, + 'parcelList': parcel_list, + 'pickupInterval': { + 'date': pickup_date + }, + 'contentDescription': description_of_content, + }, + 'shipmentNotification': { + 'trackingNotification': { + 'deliveryNotification': True, + 'problemNotification': True, + 'emails': [tracking_notific_email], + 'notificationText': '', + }, + 'recipientNotification': { + 'notificationText': '', + 'emails': [ shipment_notific_email ] + } + }, + 'labelEmail': True, + } + try: + response_data = requests.post( + url=url, + auth=(api_id, api_password), + headers=headers, + data=json.dumps(payload) + ) + response_data = json.loads(response_data.text) + if 'shipmentId' in response_data: + shipment_amount = response_data['service']['priceInfo']['totalPrice'] + awb_number = '' + url = 'https://api.letmeship.com/v1/shipments/{id}'.format(id=response_data['shipmentId']) + tracking_response = requests.get(url, auth=(api_id, api_password),headers=headers) + tracking_response_data = json.loads(tracking_response.text) + if 'trackingData' in tracking_response_data: + for parcel in tracking_response_data['trackingData']['parcelList']: + if 'awbNumber' in parcel: + awb_number = parcel['awbNumber'] + return { + 'service_provider': LETMESHIP_PROVIDER, + 'shipment_id': response_data['shipmentId'], + 'carrier': service_info['carrier'], + 'carrier_service': service_info['service_name'], + 'shipment_amount': shipment_amount, + 'awb_number': awb_number, + } + elif 'message' in response_data: + frappe.throw( + _('Error occurred while creating Shipment: {0}') + .format(response_data['message']) + ) + except Exception as exc: + frappe.msgprint( + _('Error occurred while creating Shipment: {0}') + .format(str(exc)), + indicator='orange', + alert=True + ) + + +def get_letmeship_label(shipment_id): + # Retrieve shipment label from LetMeShip + api_id = frappe.db.get_single_value('LetMeShip','api_id') + api_password = frappe.db.get_single_value('LetMeShip','api_password') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Access-Control-Allow-Origin': 'string' + } + url = 'https://api.letmeship.com/v1/shipments/{id}/documents?types=LABEL'\ + .format(id=shipment_id) + shipment_label_response = requests.get( + url, + auth=(api_id,api_password), + headers=headers + ) + shipment_label_response_data = json.loads(shipment_label_response.text) + if 'documents' in shipment_label_response_data: + for label in shipment_label_response_data['documents']: + if 'data' in label: + return json.dumps(label['data']) + else: + frappe.throw( + _('Error occurred while printing Shipment: {0}') + .format(shipment_label_response_data['message']) + ) + + +def get_letmeship_tracking_data(shipment_id): + # return letmeship tracking data + api_id = frappe.db.get_single_value('LetMeShip','api_id') + api_password = frappe.db.get_single_value('LetMeShip','api_password') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Access-Control-Allow-Origin': 'string' + } + try: + url = 'https://api.letmeship.com/v1/tracking?shipmentid={id}'.format(id=shipment_id) + tracking_data_response = requests.get( + url, + auth=(api_id, api_password), + headers=headers + ) + tracking_data = json.loads(tracking_data_response.text) + if 'awbNumber' in tracking_data: + tracking_status = 'In Progress' + if tracking_data['lmsTrackingStatus'].startswith('DELIVERED'): + tracking_status = 'Delivered' + if tracking_data['lmsTrackingStatus'] == 'RETURNED': + tracking_status = 'Returned' + if tracking_data['lmsTrackingStatus'] == 'LOST': + tracking_status = 'Lost' + tracking_url = get_tracking_url( + carrier=tracking_data['carrier'], + tracking_number=tracking_data['awbNumber'] + ) + return { + 'awb_number': tracking_data['awbNumber'], + 'tracking_status': tracking_status, + 'tracking_status_info': tracking_data['lmsTrackingStatus'], + 'tracking_url': tracking_url, + } + elif 'message' in tracking_data: + frappe.throw( + _('Error occurred while updating Shipment: {0}') + .format(tracking_data['message']) + ) + except Exception as exc: + frappe.msgprint( + _('Error occurred while updating Shipment: {0}') + .format(str(exc)), + indicator='orange', + alert=True + ) + + +def get_parcel_list(shipment_parcel, description_of_content): + parcel_list = [] + for parcel in shipment_parcel: + formatted_parcel = {} + formatted_parcel['height'] = parcel.get('height') + formatted_parcel['width'] = parcel.get('width') + formatted_parcel['length'] = parcel.get('length') + formatted_parcel['weight'] = parcel.get('weight') + formatted_parcel['quantity'] = parcel.get('count') + formatted_parcel['contentDescription'] = description_of_content + parcel_list.append(formatted_parcel) + return parcel_list + +def set_letmeship_specific_fields(pickup_contact, delivery_contact): + pickup_contact.phone_prefix = pickup_contact.phone[:3] + pickup_contact.phone = re.sub('[^A-Za-z0-9]+', '', pickup_contact.phone[3:]) + + pickup_contact.title = 'MS' + if pickup_contact.gender == 'Male': + pickup_contact.title = 'MR' + + delivery_contact.phone_prefix = delivery_contact.phone[:3] + delivery_contact.phone = re.sub('[^A-Za-z0-9]+', '', delivery_contact.phone[3:]) + + delivery_contact.title = 'MS' + if delivery_contact.gender == 'Male': + delivery_contact.title = 'MR' \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py new file mode 100644 index 0000000000..3439e4fd72 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLetMeShip(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/packlink/__init__.py b/erpnext/erpnext_integrations/doctype/packlink/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.js b/erpnext/erpnext_integrations/doctype/packlink/packlink.js new file mode 100644 index 0000000000..da864584f6 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Packlink', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.json b/erpnext/erpnext_integrations/doctype/packlink/packlink.json new file mode 100644 index 0000000000..a56595e9a1 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "creation": "2020-07-22 10:45:17.672439", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "api_key" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "read_only_depends_on": "eval:doc.enabled == 0" + } + ], + "issingle": 1, + "links": [], + "modified": "2020-08-05 16:33:59.720980", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "Packlink", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.py b/erpnext/erpnext_integrations/doctype/packlink/packlink.py new file mode 100644 index 0000000000..7fdb053cf8 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import frappe +import requests +from frappe import _ +from frappe.model.document import Document +from erpnext.erpnext_integrations.utils import get_tracking_url + +PACKLINK_PROVIDER = 'Packlink' + +class Packlink(Document): + pass + +def get_packlink_available_services(pickup_address, delivery_address, shipment_parcel,pickup_date): + # Retrieve rates at PackLink from specification stated. + from_zip = pickup_address.pincode + from_country_code = pickup_address.country_code + to_zip = delivery_address.pincode + to_country_code = delivery_address.country_code + shipment_parcel_params = '' + parcel_list = packlink_get_parcel_list(json.loads(shipment_parcel)) + for (index, parcel) in enumerate(parcel_list): + shipment_parcel_params += 'packages[{index}][height]={height}&packages[{index}][length]={length}&packages[{index}][weight]={weight}&packages[{index}][width]={width}&'.format( + index=index, + height=parcel['height'], + length=parcel['length'], + weight=parcel['weight'], + width=parcel['width'] + ) + url = 'https://api.packlink.com/v1/services?from[country]={}&from[zip]={}&to[country]={}&to[zip]={}&{}sortBy=totalPrice&source=PRO'.format( + from_country_code, + from_zip, + to_country_code, + to_zip, + shipment_parcel_params + ) + api_key = frappe.db.get_single_value('Packlink', 'api_key') + enabled = frappe.db.get_single_value('Packlink', 'enabled') + if not api_key or not enabled: + return [] + try: + responses = requests.get(url, headers={'Authorization': api_key}) + responses_dict = json.loads(responses.text) + # If an error occured on the api. Show the error message + if 'messages' in responses_dict: + frappe.msgprint( + _('Packlink: {0}' + .format(str(responses_dict['messages'][0]['message'])) + ), + indicator='orange', + alert=True + ) + available_services = [] + for response in responses_dict: + if parse_pickup_date(pickup_date) \ + in response['available_dates'].keys(): + available_service = frappe._dict() + available_service.service_provider = PACKLINK_PROVIDER + available_service.carrier = response['carrier_name'] + available_service.carrier_name = response['name'] + available_service.service_name = '' + available_service.is_preferred = 0 + available_service.total_price = response['price']['base_price'] + available_service.actual_price = response['price']['total_price'] + available_service.service_id = response['id'] + available_service.available_dates = response['available_dates'] + available_services.append(available_service) + + return available_services + except Exception as exc: + frappe.msgprint( + _('Error occurred on Packlink: {0}') + .format(str(exc)), indicator='orange', + alert=True + ) + return [] + + +def create_packlink_shipment(pickup_address, delivery_address, shipment_parcel, + description_of_content, pickup_date, value_of_goods, pickup_contact, + delivery_contact, service_info): + # Create a transaction at PackLink + enabled = frappe.db.get_single_value('Packlink', 'enabled') + if not enabled: + frappe.throw(_('Packlink integration is not enabled')) + api_key = frappe.db.get_single_value('Packlink', 'api_key') + from_country_code = pickup_address.country_code + to_country_code = delivery_address.country_code + data = { + 'additional_data': { + 'postal_zone_id_from': '', + 'postal_zone_name_from': pickup_address.country, + 'postal_zone_id_to': '', + 'postal_zone_name_to': delivery_address.country, + }, + 'collection_date': parse_pickup_date(pickup_date), + 'collection_time': '', + 'content': description_of_content, + 'contentvalue': value_of_goods, + 'content_second_hand': False, + 'from': { + 'city': pickup_address.city, + 'company': pickup_address.address_title, + 'country': from_country_code, + 'email': pickup_contact.email, + 'name': pickup_contact.first_name, + 'phone': pickup_contact.phone, + 'state': pickup_address.country, + 'street1': pickup_address.address_line1, + 'street2': pickup_address.address_line2, + 'surname': pickup_contact.last_name, + 'zip_code': pickup_address.pincode, + }, + 'insurance': {'amount': 0, 'insurance_selected': False}, + 'price': {}, + 'packages': packlink_get_parcel_list(json.loads(shipment_parcel)), + 'service_id': service_info['service_id'], + 'to': { + 'city': delivery_address.city, + 'company': delivery_address.address_title, + 'country': to_country_code, + 'email': delivery_contact.email, + 'name': delivery_contact.first_name, + 'phone': delivery_contact.phone, + 'state': delivery_address.country, + 'street1': delivery_address.address_line1, + 'street2': delivery_address.address_line2, + 'surname': delivery_contact.last_name, + 'zip_code': delivery_address.pincode, + }, + } + + url = 'https://api.packlink.com/v1/shipments' + headers = { + 'Authorization': api_key, + 'Content-Type': 'application/json' + } + try: + response_data = requests.post(url, json=data, headers=headers) + response_data = json.loads(response_data.text) + if 'reference' in response_data: + return { + 'service_provider': PACKLINK_PROVIDER, + 'shipment_id': response_data['reference'], + 'carrier': service_info['carrier'], + 'carrier_service': service_info['service_name'], + 'shipment_amount': service_info['actual_price'], + 'awb_number': '', + } + except Exception as exc: + frappe.msgprint( + _('Error occurred while creating Shipment: {0}') + .format(str(exc)), + indicator='orange', + alert=True + ) + + +def get_packlink_label(shipment_id): + # Retrieve shipment label from PackLink + enabled = frappe.db.get_single_value('Packlink', 'enabled') + if not enabled: + frappe.throw(_('Packlink integration is not enabled')) + api_key = frappe.db.get_single_value('Packlink', 'api_key') + headers = { + 'Authorization': api_key, + 'Content-Type': 'application/json' + } + shipment_label_response = requests.get( + 'https://api.packlink.com/v1/shipments/{id}/labels'.format(id=shipment_id), + headers=headers + ) + shipment_label = json.loads(shipment_label_response.text) + if shipment_label: + return shipment_label + else: + frappe.msgprint(_('Shipment ID not found')) + + +def get_packlink_tracking_data(shipment_id): + # Get Packlink Tracking Info + enabled = frappe.db.get_single_value('Packlink', 'enabled') + if not enabled: + frappe.throw(_('Packlink integration is not enabled')) + api_key = frappe.db.get_single_value('Packlink', 'api_key') + headers = { + 'Authorization': api_key, + 'Content-Type': 'application/json' + } + try: + url = 'https://api.packlink.com/v1/shipments/{id}'.format(id=shipment_id) + tracking_data_response = requests.get(url, headers=headers) + tracking_data = json.loads(tracking_data_response.text) + if 'trackings' in tracking_data: + tracking_status = 'In Progress' + if tracking_data['state'] == 'DELIVERED': + tracking_status = 'Delivered' + if tracking_data['state'] == 'RETURNED': + tracking_status = 'Returned' + if tracking_data['state'] == 'LOST': + tracking_status = 'Lost' + awb_number = None if not tracking_data['trackings'] else tracking_data['trackings'][0] + tracking_url = get_tracking_url( + carrier=tracking_data['carrier'], + tracking_number=awb_number + ) + return { + 'awb_number': awb_number, + 'tracking_status': tracking_status, + 'tracking_status_info': tracking_data['state'], + 'tracking_url': tracking_url + } + except Exception as exc: + frappe.msgprint(_('Error occurred while updating Shipment: {0}').format( + str(exc)), indicator='orange', alert=True) + return [] + + +def packlink_get_parcel_list(shipment_parcel): + parcel_list = [] + for parcel in shipment_parcel: + for count in range(parcel.get('count')): + formatted_parcel = {} + formatted_parcel['height'] = parcel.get('height') + formatted_parcel['width'] = parcel.get('width') + formatted_parcel['length'] = parcel.get('length') + formatted_parcel['weight'] = parcel.get('weight') + parcel_list.append(formatted_parcel) + return parcel_list + + +def parse_pickup_date(pickup_date): + return pickup_date.replace('-', '/') \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py b/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py new file mode 100644 index 0000000000..106ae51f7c --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPacklink(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py b/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js new file mode 100644 index 0000000000..3b85236863 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('SendCloud', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json new file mode 100644 index 0000000000..dab54cba6c --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json @@ -0,0 +1,56 @@ +{ + "actions": [], + "creation": "2020-08-18 09:48:50.836233", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "api_key", + "api_secret" + ], + "fields": [ + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "read_only_depends_on": "eval:doc.enabled == 0" + }, + { + "fieldname": "api_secret", + "fieldtype": "Data", + "label": "API Secret", + "read_only_depends_on": "eval:doc.enabled == 0" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-08-18 09:48:50.836233", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "SendCloud", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py new file mode 100644 index 0000000000..85c94388dc --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import requests +import frappe +import json +from frappe import _ +from frappe.model.document import Document + +SENDCLOUD_PROVIDER = 'SendCloud' + +class SendCloud(Document): + pass + +def get_sendcloud_available_services(delivery_address, shipment_parcel): + # Retrieve rates at SendCloud from specification stated. + enabled = frappe.db.get_single_value('SendCloud', 'enabled') + api_key = frappe.db.get_single_value('SendCloud', 'api_key') + api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') + if not enabled or not api_key or not api_secret: + return [] + + try: + url = 'https://panel.sendcloud.sc/api/v2/shipping_methods' + responses = requests.get(url, auth=(api_key, api_secret)) + responses_dict = json.loads(responses.text) + + available_services = [] + for service in responses_dict['shipping_methods']: + for country in service['countries']: + if country['iso_2'] == delivery_address.country_code: + available_service = frappe._dict() + available_service.service_provider = 'SendCloud' + available_service.carrier = service['carrier'] + available_service.service_name = service['name'] + available_service.total_price = total_parcel_price(country['price'], json.loads(shipment_parcel)) + available_service.service_id = service['id'] + available_services.append(available_service) + return available_services + except Exception as exc: + frappe.msgprint(_('Error occurred on SendCloud: {0}').format( + str(exc)), indicator='orange', alert=True) + +def create_sendcloud_shipment( + shipment, + delivery_address, + delivery_contact, + service_info, + shipment_parcel, + description_of_content, + value_of_goods +): + # Create a transaction at SendCloud + enabled = frappe.db.get_single_value('SendCloud', 'enabled') + api_key = frappe.db.get_single_value('SendCloud', 'api_key') + api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') + if not enabled or not api_key or not api_secret: + return [] + + parcels = [] + for i, parcel in enumerate(json.loads(shipment_parcel), start=1): + parcel_data = { + 'name': "{} {}".format(delivery_contact.first_name, delivery_contact.last_name), + 'company_name': delivery_address.address_title, + 'address': delivery_address.address_line1, + 'address_2': delivery_address.address_line2 or '', + 'city': delivery_address.city, + 'postal_code': delivery_address.pincode, + 'telephone': delivery_contact.phone, + 'request_label': True, + 'email': delivery_contact.email, + 'data': [], + 'country': delivery_address.country_code, + 'shipment': { + 'id': service_info['service_id'] + }, + 'order_number': "{}-{}".format(shipment, i), + 'external_reference': "{}-{}".format(shipment, i), + 'weight': parcel.get('weight'), + 'parcel_items': get_parcel_items(parcel, description_of_content, value_of_goods) + } + parcels.append(parcel_data) + data = { + 'parcels': parcels + } + try: + url = 'https://panel.sendcloud.sc/api/v2/parcels?errors=verbose' + response_data = requests.post(url, json=data, auth=(api_key, api_secret)) + response_data = json.loads(response_data.text) + if 'failed_parcels' in response_data: + frappe.msgprint(_('Error occurred while creating Shipment: {0}' + ).format(response_data['failed_parcels'][0]['errors']), indicator='orange', + alert=True) + else: + shipment_id = ', '.join([str(x['id']) for x in response_data['parcels']]) + awb_number = ', '.join([str(x['tracking_number']) for x in response_data['parcels']]) + return { + 'service_provider': 'SendCloud', + 'shipment_id': shipment_id, + 'carrier': service_info['carrier'], + 'carrier_service': service_info['service_name'], + 'shipment_amount': service_info['total_price'], + 'awb_number': awb_number + } + except Exception as exc: + frappe.msgprint(_('Error occurred while creating Shipment: {0}').format( + str(exc)), indicator='orange', alert=True) + +def get_sendcloud_label(shipment_id): + # Retrieve shipment label from SendCloud + api_key = frappe.db.get_single_value('SendCloud', 'api_key') + api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') + shipment_id_list = shipment_id.split(', ') + label_urls = [] + for ship_id in shipment_id_list: + shipment_label_response = \ + requests.get('https://panel.sendcloud.sc/api/v2/labels/{id}'.format(id=ship_id), auth=(api_key, api_secret)) + shipment_label = json.loads(shipment_label_response.text) + label_urls.append(shipment_label['label']['label_printer']) + if len(label_urls): + return label_urls + else: + frappe.msgprint(_('Shipment ID not found')) + +def get_sendcloud_tracking_data(shipment_id): + # return SendCloud tracking data + try: + api_key = frappe.db.get_single_value('SendCloud', 'api_key') + api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') + shipment_id_list = shipment_id.split(', ') + tracking_url = '' + awb_number = [] + tracking_status = [] + tracking_status_info = [] + for ship_id in shipment_id_list: + tracking_data_response = \ + requests.get('https://panel.sendcloud.sc/api/v2/parcels/{id}'.format(id=ship_id), auth=(api_key, api_secret)) + tracking_data = json.loads(tracking_data_response.text) + tracking_url_template = \ + '{{ _("Click here to Track Shipment") }}
' + tracking_url += frappe.render_template(tracking_url_template, {'tracking_url': tracking_data['parcel']['tracking_url']}) + awb_number.append(tracking_data['parcel']['tracking_number']) + tracking_status.append(tracking_data['parcel']['status']['message']) + tracking_status_info.append(tracking_data['parcel']['status']['message']) + return { + 'awb_number': ', '.join(awb_number), + 'tracking_status': ', '.join(tracking_status), + 'tracking_status_info': ', '.join(tracking_status_info), + 'tracking_url': tracking_url + } + except Exception as exc: + frappe.msgprint(_('Error occurred while updating Shipment: {0}').format( + str(exc)), indicator='orange', alert=True) + +def total_parcel_price(parcel_price, shipment_parcel): + count = 0 + for parcel in shipment_parcel: + count += parcel.get('count') + return parcel_price * count + +def get_parcel_items(parcel, description_of_content, value_of_goods): + parcel_list = [] + formatted_parcel = {} + formatted_parcel['description'] = description_of_content + formatted_parcel['quantity'] = parcel.get('count') + formatted_parcel['weight'] = parcel.get('weight') + formatted_parcel['value'] = value_of_goods + parcel_list.append(formatted_parcel) + return parcel_list \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py new file mode 100644 index 0000000000..5cbe80e8ac --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSendCloud(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index e278fd7807..e7ef4c8ebd 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -60,4 +60,14 @@ def create_mode_of_payment(gateway, payment_type="General"): "default_account": payment_gateway_account }] }) - mode_of_payment.insert(ignore_permissions=True) \ No newline at end of file + mode_of_payment.insert(ignore_permissions=True) + +def get_tracking_url(carrier, tracking_number): + # Return the formatted Tracking URL. + tracking_url = '' + url_reference = frappe.get_value('Parcel Service', carrier, 'url_reference') + if url_reference: + tracking_url = frappe.render_template(url_reference, {'tracking_number': tracking_number}) + tracking_url_template = '{{ _("Click here to Track Shipment") }}' + tracking_url = frappe.render_template(tracking_url_template, {'tracking_url': tracking_url}) + return tracking_url diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 251a26a592..03921c554e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -156,6 +156,11 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } if (!doc.is_return && doc.status!="Closed") { + if(doc.docstatus == 1) { + this.frm.add_custom_button(__('Shipment'), function() { + me.make_shipment() }, __('Create')); + } + if(flt(doc.per_installed, 2) < 100 && doc.docstatus==1) this.frm.add_custom_button(__('Installation Note'), function() { me.make_installation_note() }, __('Create')); @@ -220,6 +225,13 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }, + make_shipment: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.delivery_note.delivery_note.make_shipment", + frm: this.frm + }) + }, + make_sales_invoice: function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d04cf785ab..00a66fa48e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -569,6 +569,59 @@ def make_packing_slip(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_shipment(source_name, target_doc=None): + def postprocess(source, target): + user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) + target.pickup_contact_email = user.email + pickup_contact_display = '{}'.format(user.full_name) + if user.email: + pickup_contact_display += '
' + user.email + if user.phone: + pickup_contact_display += '
' + user.phone + if user.mobile_no and not user.phone: + pickup_contact_display += '
' + user.mobile_no + target.pickup_contact = pickup_contact_display + + contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) + delivery_contact_display = '{}'.format(source.contact_display) + if contact.email_id: + delivery_contact_display += '
' + contact.email_id + if contact.phone: + delivery_contact_display += '
' + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += '
' + contact.mobile_no + target.delivery_contact = delivery_contact_display + + doclist = get_mapped_doc("Delivery Note", source_name, { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "address_display": "delivery_address", + "customer": "delivery_customer", + "shipping_address_name": "delivery_address_name", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email" + }, + "validation": { + "docstatus": ["=", 1] + } + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Notes", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + } + } + }, target_doc, postprocess) + + return doclist @frappe.whitelist() def make_sales_return(source_name, target_doc=None): diff --git a/erpnext/stock/doctype/parcel_service/__init__.py b/erpnext/stock/doctype/parcel_service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.js b/erpnext/stock/doctype/parcel_service/parcel_service.js new file mode 100644 index 0000000000..43b8ed5bf8 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service/parcel_service.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Parcel Service', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.json b/erpnext/stock/doctype/parcel_service/parcel_service.json new file mode 100644 index 0000000000..9960acf4ae --- /dev/null +++ b/erpnext/stock/doctype/parcel_service/parcel_service.json @@ -0,0 +1,56 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:parcel_service_name", + "creation": "2020-07-23 10:35:38.211715", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parcel_service_name", + "parcel_service_code", + "url_reference" + ], + "fields": [ + { + "fieldname": "parcel_service_name", + "fieldtype": "Data", + "label": "Parcel Service Name", + "unique": 1 + }, + { + "fieldname": "parcel_service_code", + "fieldtype": "Data", + "label": "Parcel Service Code" + }, + { + "fieldname": "url_reference", + "fieldtype": "Data", + "label": "URL Reference" + } + ], + "links": [], + "modified": "2020-07-23 10:35:38.211715", + "modified_by": "Administrator", + "module": "Stock", + "name": "Parcel Service", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.py b/erpnext/stock/doctype/parcel_service/parcel_service.py new file mode 100644 index 0000000000..e46ac76ef7 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service/parcel_service.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ParcelService(Document): + pass diff --git a/erpnext/stock/doctype/parcel_service/test_parcel_service.py b/erpnext/stock/doctype/parcel_service/test_parcel_service.py new file mode 100644 index 0000000000..c2f96d9cb0 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service/test_parcel_service.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestParcelService(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/parcel_service_type/__init__.py b/erpnext/stock/doctype/parcel_service_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js new file mode 100644 index 0000000000..31d54536c0 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js @@ -0,0 +1,12 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Parcel Service Type Alias', { + parcel_type_alias: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.parcel_type_alias) { + frappe.model.set_value(cdt, cdn, 'parcel_service', frm.doc.parcel_service); + frm.refresh_field('parcel_service_type_alias'); + } + } +}); diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json new file mode 100644 index 0000000000..3c0c4d5f80 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format: {parcel_service} - {parcel_service_type}", + "creation": "2020-07-23 10:47:43.794083", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parcel_service", + "parcel_service_type", + "description", + "section_break_4", + "parcel_service_type_alias", + "column_break_6", + "section_break_7", + "show_in_preferred_services_list" + ], + "fields": [ + { + "fieldname": "parcel_service", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Parcel Service", + "options": "Parcel Service", + "reqd": 1 + }, + { + "fieldname": "parcel_service_type", + "fieldtype": "Data", + "label": "Parcel Service Type", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "parcel_service_type_alias", + "fieldtype": "Table", + "label": "Parcel Service Type Alias", + "options": "Parcel Service Type Alias" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "show_in_preferred_services_list", + "fieldtype": "Check", + "label": "Show in Preferred Services List" + } + ], + "links": [], + "modified": "2020-07-23 10:47:43.794083", + "modified_by": "Administrator", + "module": "Stock", + "name": "Parcel Service Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py new file mode 100644 index 0000000000..b55528c359 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ParcelServiceType(Document): + pass + +def match_parcel_service_type_alias(parcel_service_type, parcel_service): + # Match and return Parcel Service Type Alias to Parcel Service Type if exists. + if frappe.db.exists('Parcel Service', parcel_service): + matched_parcel_service_type = \ + frappe.db.get_value('Parcel Service Type Alias', { + 'parcel_type_alias': parcel_service_type, + 'parcel_service': parcel_service + }, 'parent') + if matched_parcel_service_type: + parcel_service_type = matched_parcel_service_type + return parcel_service_type diff --git a/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py b/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py new file mode 100644 index 0000000000..e214264acc --- /dev/null +++ b/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestParcelServiceType(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/parcel_service_type_alias/__init__.py b/erpnext/stock/doctype/parcel_service_type_alias/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json new file mode 100644 index 0000000000..8e7731e6c1 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json @@ -0,0 +1,41 @@ +{ + "actions": [], + "creation": "2020-07-23 10:47:23.626510", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parcel_service", + "parcel_type_alias" + ], + "fields": [ + { + "fieldname": "parcel_service", + "fieldtype": "Link", + "hidden": 1, + "in_list_view": 1, + "label": "Parcel Service", + "options": "Parcel Service", + "read_only": 1 + }, + { + "fieldname": "parcel_type_alias", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Parcel Type Alias", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-23 10:47:23.626510", + "modified_by": "Administrator", + "module": "Stock", + "name": "Parcel Service Type Alias", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py new file mode 100644 index 0000000000..fd0a7d8b49 --- /dev/null +++ b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ParcelServiceTypeAlias(Document): + pass diff --git a/erpnext/stock/doctype/shipment/__init__.py b/erpnext/stock/doctype/shipment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment/api/utils.py b/erpnext/stock/doctype/shipment/api/utils.py new file mode 100644 index 0000000000..1153933e81 --- /dev/null +++ b/erpnext/stock/doctype/shipment/api/utils.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +import re + +def get_address(address_name): + address = frappe.db.get_value('Address', address_name, [ + 'address_title', + 'address_line1', + 'address_line2', + 'city', + 'pincode', + 'country', + ], as_dict=1) + address.country_code = frappe.db.get_value('Country', address.country, 'code').upper() + if not address.pincode or address.pincode == '': + frappe.throw(_("Postal Code is mandatory to continue.
\ + Please set Postal Code for Address {1}" + ).format(address_name, address_name)) + address.pincode = address.pincode.replace(' ', '') + address.city = address.city.strip() + return address + +def get_contact(contact_name): + contact = frappe.db.get_value('Contact', contact_name, [ + 'first_name', + 'last_name', + 'email_id', + 'phone', + 'mobile_no', + 'gender', + ], as_dict=1) + if not contact.last_name: + frappe.throw(_("Last Name is mandatory to continue.
\ + Please set Last Name for Contact {1}" + ).format(contact_name, contact_name)) + if not contact.phone: + contact.phone = contact.mobile_no + contact.phone_prefix = contact.phone[:3] + contact.phone = re.sub('[^A-Za-z0-9]+', '', contact.phone[3:]) + contact.email = contact.email_id + contact.title = 'MS' + if contact.gender == 'Male': + contact.title = 'MR' + return contact + +def get_company_contact(): + contact = frappe.db.get_value('User', frappe.session.user, [ + 'first_name', + 'last_name', + 'email', + 'phone', + 'mobile_no', + 'gender', + ], as_dict=1) + if not contact.phone: + contact.phone = contact.mobile_no + contact.phone_prefix = contact.phone[:3] + contact.phone = re.sub('[^A-Za-z0-9]+', '', contact.phone[3:]) + contact.title = 'MS' + if contact.gender == 'Male': + contact.title = 'MR' + return contact diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js new file mode 100644 index 0000000000..e9f4484ab1 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -0,0 +1,772 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Shipment', { + setup: function(frm) { + if (frm.doc.__islocal) { + frm.trigger('pickup_type'); + } + }, + address_query: function(frm, link_doctype, link_name, is_your_company_address) { + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { + link_doctype: link_doctype, + link_name: link_name, + is_your_company_address: is_your_company_address + } + }; + }, + contact_query: function(frm, link_doctype, link_name) { + return { + query: 'frappe.contacts.doctype.contact.contact.contact_query', + filters: { + link_doctype: link_doctype, + link_name: link_name + } + }; + }, + onload: function(frm) { + frm.set_query("delivery_address_name", () => { + let link_doctype = ''; + let link_name = ''; + let is_your_company_address = 0; + if (frm.doc.delivery_to_type == 'Customer') { + link_doctype = 'Customer'; + link_name = frm.doc.delivery_customer; + } + if (frm.doc.delivery_to_type == 'Supplier') { + link_doctype = 'Supplier'; + link_name = frm.doc.delivery_supplier; + } + if (frm.doc.delivery_to_type == 'Company') { + link_doctype = 'Company'; + link_name = frm.doc.delivery_company; + is_your_company_address = 1; + } + return frm.events.address_query(frm, link_doctype, link_name, is_your_company_address); + }); + frm.set_query("pickup_address_name", () => { + let link_doctype = ''; + let link_name = ''; + let is_your_company_address = 0; + if (frm.doc.pickup_from_type == 'Customer') { + link_doctype = 'Customer'; + link_name = frm.doc.pickup_customer; + } + if (frm.doc.pickup_from_type == 'Supplier') { + link_doctype = 'Supplier'; + link_name = frm.doc.pickup_supplier; + } + if (frm.doc.pickup_from_type == 'Company') { + link_doctype = 'Company'; + link_name = frm.doc.pickup_company; + is_your_company_address = 1; + } + return frm.events.address_query(frm, link_doctype, link_name, is_your_company_address); + }); + frm.set_query("delivery_contact_name", () => { + let link_doctype = ''; + let link_name = ''; + if (frm.doc.delivery_to_type == 'Customer') { + link_doctype = 'Customer'; + link_name = frm.doc.delivery_customer; + } + if (frm.doc.delivery_to_type == 'Supplier') { + link_doctype = 'Supplier'; + link_name = frm.doc.delivery_supplier; + } + if (frm.doc.delivery_to_type == 'Company') { + link_doctype = 'Company'; + link_name = frm.doc.delivery_company; + } + return frm.events.contact_query(frm, link_doctype, link_name); + }); + frm.set_query("pickup_contact_name", () => { + let link_doctype = ''; + let link_name = ''; + if (frm.doc.pickup_from_type == 'Customer') { + link_doctype = 'Customer'; + link_name = frm.doc.pickup_customer; + } + if (frm.doc.pickup_from_type == 'Supplier') { + link_doctype = 'Supplier'; + link_name = frm.doc.pickup_supplier; + } + if (frm.doc.pickup_from_type == 'Company') { + link_doctype = 'Company'; + link_name = frm.doc.pickup_company; + } + return frm.events.contact_query(frm, link_doctype, link_name); + }); + frm.set_query("delivery_note", "shipment_delivery_notes", function() { + let customer = ''; + if (frm.doc.delivery_to_type == "Customer") { + customer = frm.doc.delivery_customer; + } + if (frm.doc.delivery_to_type == "Company") { + customer = frm.doc.delivery_company; + } + if (customer) { + return { + filters: { + customer: customer, + docstatus: 1, + status: ["not in", ["Cancelled"]] + } + }; + } + }); + }, + refresh: function(frm) { + if (frm.doc.docstatus === 1 && !frm.doc.shipment_id) { + frm.add_custom_button(__('Fetch Shipping Rates'), function() { + return frm.events.fetch_shipping_rates(frm); + }); + } + if (frm.doc.shipment_id) { + frm.add_custom_button(__('Print Shipping Label'), function() { + return frm.events.print_shipping_label(frm); + }); + if (frm.doc.tracking_status != 'Delivered') { + frm.add_custom_button(__('Update Tracking'), function() { + return frm.events.update_tracking(frm, frm.doc.service_provider, frm.doc.shipment_id); + }); + } + } + $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); + $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); + $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); + $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); + + if (frm.doc.delivery_from_type != 'Company') { + frm.set_df_property("delivery_contact_name", "reqd", 1); + } + if (frm.doc.pickup_from_type != 'Company') { + frm.set_df_property("pickup_contact_name", "reqd", 1); + } + else { + frm.toggle_display("pickup_contact_name", false); + } + }, + before_save: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + frm.set_value("delivery_to", frm.doc.delivery_company); + } + if (frm.doc.delivery_to_type == 'Customer') { + frm.set_value("delivery_to", frm.doc.delivery_customer); + } + if (frm.doc.delivery_to_type == 'Supplier') { + frm.set_value("delivery_to", frm.doc.delivery_supplier); + } + if (frm.doc.pickup_from_type == 'Company') { + frm.set_value("pickup", frm.doc.pickup_company); + } + if (frm.doc.pickup_from_type == 'Customer') { + frm.set_value("pickup", frm.doc.pickup_customer); + } + if (frm.doc.pickup_from_type == 'Supplier') { + frm.set_value("pickup", frm.doc.pickup_supplier); + } + }, + set_pickup_company_address: function(frm) { + frappe.db.get_value('Address', { + address_title: frm.doc.pickup_company, + is_your_company_address: 1 + }, 'name', (r) => { + frm.set_value("pickup_address_name", r.name); + }); + }, + set_delivery_company_address: function(frm) { + frappe.db.get_value('Address', { + address_title: frm.doc.delivery_company, + is_your_company_address: 1 + }, 'name', (r) => { + frm.set_value("delivery_address_name", r.name); + }); + }, + pickup_from_type: function(frm) { + if (frm.doc.pickup_from_type == 'Company') { + frm.set_value("pickup_company", frappe.defaults.get_default('company')); + frm.set_df_property("pickup_contact_name", "reqd", 0); + frm.set_value("pickup_customer", ''); + frm.set_value("pickup_supplier", ''); + frm.toggle_display("pickup_contact_name", false); + } + else { + frm.set_df_property("pickup_contact_name", "reqd", 1); + frm.toggle_display("pickup_contact_name", true); + frm.trigger('clear_pickup_fields'); + } + if (frm.doc.pickup_from_type == 'Customer') { + frm.set_value("pickup_company", ''); + frm.set_value("pickup_supplier", ''); + } + if (frm.doc.pickup_from_type == 'Supplier') { + frm.set_value("pickup_customer", ''); + frm.set_value("pickup_company", ''); + } + frm.events.remove_notific_child_table(frm, 'shipment_notification_subscriptions', 'Pickup'); + frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscriptions', 'Pickup'); + }, + delivery_to_type: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + frm.set_value("delivery_company", frappe.defaults.get_default('company')); + frm.set_df_property("delivery_contact_name", "reqd", 0); + frm.set_value("delivery_customer", ''); + frm.set_value("delivery_supplier", ''); + frm.toggle_display("delivery_contact_name", false); + } + else { + frm.set_df_property("delivery_contact_name", "reqd", 1); + frm.toggle_display("delivery_contact_name", true); + frm.trigger('clear_delivery_fields'); + } + if (frm.doc.delivery_to_type == 'Customer') { + frm.set_value("delivery_company", ''); + frm.set_value("delivery_supplier", ''); + } + if (frm.doc.delivery_to_type == 'Supplier') { + frm.set_value("delivery_customer", ''); + frm.set_value("delivery_company", ''); + frm.toggle_display("shipment_delivery_notes", false); + } + else { + frm.toggle_display("shipment_delivery_notes", true); + } + frm.events.remove_notific_child_table(frm, 'shipment_notification_subscriptions', 'Delivery'); + frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscriptions', 'Delivery'); + }, + delivery_address_name: function(frm) { + if (frm.doc.delivery_to_type == 'Company') { + erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true); + } + else { + erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false); + } + }, + pickup_address_name: function(frm) { + if (frm.doc.pickup_from_type == 'Company') { + erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true); + } + else { + erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false); + } + }, + get_contact_display: function(frm, contact_name, contact_type) { + frappe.call({ + method: "frappe.contacts.doctype.contact.contact.get_contact_details", + args: { contact: contact_name }, + callback: function(r) { + if(r.message) { + if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { + if (contact_type == 'Delivery') { + frm.set_value('delivery_contact_name', ''); + frm.set_value('delivery_contact', ''); + } + else { + frm.set_value('pickup_contact_name', ''); + frm.set_value('pickup_contact', ''); + } + frappe.throw(__(`Email or Phone/Mobile of the Contact are mandatory to continue.
+ Please set Email/Phone for the contact ${contact_name}`)); + } + let contact_display = r.message.contact_display; + if (r.message.contact_email) { + contact_display += '
' + r.message.contact_email; + } + if (r.message.contact_phone) { + contact_display += '
' + r.message.contact_phone; + } + if (r.message.contact_mobile && !r.message.contact_phone) { + contact_display += '
' + r.message.contact_mobile; + } + if (contact_type == 'Delivery'){ + frm.set_value('delivery_contact', contact_display); + if (r.message.contact_email) { + frm.set_value('delivery_contact_email', r.message.contact_email); + } + } + else { + frm.set_value('pickup_contact', contact_display); + if (r.message.contact_email) { + frm.set_value('pickup_contact_email', r.message.contact_email); + } + } + } + } + }); + }, + delivery_contact_name: function(frm) { + if (frm.doc.delivery_contact_name) { + frm.events.get_contact_display(frm, frm.doc.delivery_contact_name, 'Delivery'); + } + }, + pickup_contact_name: function(frm) { + if (frm.doc.pickup_contact_name) { + frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup'); + } + }, + set_company_contact: function(frm, delivery_type) { + frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => { + if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_company', ''); + frm.set_value('delivery_contact', ''); + } + else { + frm.set_value('pickup_company', ''); + frm.set_value('pickup_contact', ''); + } + frappe.throw(__(`Last Name, Email or Phone/Mobile of the user are mandatory to continue.
+ Please first set Last Name, Email and Phone for the user ${frappe.session.user}`)); + } + let contact_display = r.full_name; + if (r.email) { + contact_display += '
' + r.email; + } + if (r.phone) { + contact_display += '
' + r.phone; + } + if (r.mobile_no && !r.phone) { + contact_display += '
' + r.mobile_no; + } + if (delivery_type == 'Delivery') { + frm.set_value('delivery_contact', contact_display); + if (r.email) { + frm.set_value('delivery_contact_email', r.email); + } + } + else { + frm.set_value('pickup_contact', contact_display); + if (r.email) { + frm.set_value('pickup_contact_email', r.email); + } + } + }); + }, + pickup_company: function(frm) { + if (frm.doc.pickup_from_type == 'Company' && frm.doc.pickup_company) { + frm.trigger('set_pickup_company_address'); + frm.events.set_company_contact(frm, 'Pickup'); + } + }, + delivery_company: function(frm) { + if (frm.doc.delivery_to_type == 'Company' && frm.doc.delivery_company) { + frm.trigger('set_delivery_company_address'); + frm.events.set_company_contact(frm, 'Delivery'); + } + }, + delivery_customer: function(frm) { + frm.trigger('clear_delivery_fields'); + if (frm.doc.delivery_customer) { + frm.events.set_address_name(frm,'Customer',frm.doc.delivery_customer, 'Delivery'); + frm.events.set_contact_name(frm,'Customer',frm.doc.delivery_customer, 'Delivery'); + } + }, + delivery_supplier: function(frm) { + frm.trigger('clear_delivery_fields'); + if (frm.doc.delivery_supplier) { + frm.events.set_address_name(frm,'Supplier',frm.doc.delivery_supplier, 'Delivery'); + frm.events.set_contact_name(frm,'Supplier',frm.doc.delivery_supplier, 'Delivery'); + } + }, + pickup_customer: function(frm) { + frm.trigger('clear_pickup_fields'); + if (frm.doc.pickup_customer) { + frm.events.set_address_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup'); + frm.events.set_contact_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup'); + } + }, + pickup_supplier: function(frm) { + frm.trigger('clear_pickup_fields'); + if (frm.doc.pickup_supplier) { + frm.events.set_address_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup'); + frm.events.set_contact_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup'); + } + }, + set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_address_name", + args: { + ref_doctype: ref_doctype, + docname: ref_docname + }, + callback: function(r) { + if(r.message) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_address_name', r.message); + } + else { + frm.set_value('pickup_address_name', r.message); + } + } + } + }); + }, + set_contact_name: function(frm, ref_doctype, ref_docname, delivery_type) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_contact_name", + args: { + ref_doctype: ref_doctype, + docname: ref_docname + }, + callback: function(r) { + if(r.message) { + if (delivery_type == 'Delivery') { + frm.set_value('delivery_contact_name', r.message); + } + else { + frm.set_value('pickup_contact_name', r.message); + } + } + } + }); + }, + add_template: function(frm) { + if (frm.doc.parcel_template) { + frappe.model.with_doc("Shipment Parcel Template", frm.doc.parcel_template, () => { + let parcel_template = frappe.model.get_doc("Shipment Parcel Template", frm.doc.parcel_template); + let row = frappe.model.add_child(frm.doc, "Shipment Parcel", "shipment_parcel"); + row.length = parcel_template.length; + row.width = parcel_template.width; + row.height = parcel_template.height; + row.weight = parcel_template.weight; + frm.refresh_fields("shipment_parcel"); + }); + } + }, + pickup_date: function(frm) { + if (frm.doc.pickup_date < frappe.datetime.get_today()) { + frappe.throw(__("Pickup Date cannot be in the past")); + } + if (frm.doc.pickup_date == frappe.datetime.get_today()) { + var pickup_time = frm.events.get_pickup_time(frm); + frm.set_value("pickup_from", pickup_time); + frm.trigger('set_pickup_to_time'); + } + }, + pickup_from: function(frm) { + var pickup_time = frm.events.get_pickup_time(frm); + if (frm.doc.pickup_from && frm.doc.pickup_date == frappe.datetime.get_today()) { + let current_hour = pickup_time.split(':')[0]; + let current_min = pickup_time.split(':')[1]; + let pickup_hour = frm.doc.pickup_from.split(':')[0]; + let pickup_min = frm.doc.pickup_from.split(':')[1]; + if (pickup_hour < current_hour || (pickup_hour == current_hour && pickup_min < current_min)) { + frm.set_value("pickup_from", pickup_time); + frappe.throw(__("Pickup Time cannot be in the past")); + } + } + frm.trigger('set_pickup_to_time'); + }, + get_pickup_time: function() { + let current_hour = new Date().getHours(); + let current_min = new Date().toLocaleString('en-US', {minute: 'numeric'}); + if (current_min < 30) { + current_min = '30'; + } + else { + current_min = '00'; + current_hour = Number(current_hour)+1; + } + if (Number(current_hour) > 19 || Number(current_hour) === 19){ + frappe.throw(__("Today's pickup time is over, please select different date")); + } + current_hour = (current_hour < 10) ? '0' + current_hour : current_hour; + let pickup_time = current_hour +':'+ current_min; + return pickup_time; + }, + set_pickup_to_time: function(frm) { + let pickup_to_hour = Number(frm.doc.pickup_from.split(':')[0])+5; + if (Number(pickup_to_hour) > 19 || Number(pickup_to_hour) === 19){ + pickup_to_hour = 19; + } + let pickup_to_min = frm.doc.pickup_from.split(':')[1]; + let pickup_to = pickup_to_hour +':'+ pickup_to_min; + frm.set_value("pickup_to", pickup_to); + }, + clear_pickup_fields: function(frm) { + frm.set_value("pickup_address_name", ''); + frm.set_value("pickup_contact_name", ''); + frm.set_value("pickup_address", ''); + frm.set_value("pickup_contact", ''); + frm.set_value("pickup_contact_email", ''); + }, + clear_delivery_fields: function(frm) { + frm.set_value("delivery_address_name", ''); + frm.set_value("delivery_contact_name", ''); + frm.set_value("delivery_address", ''); + frm.set_value("delivery_contact", ''); + frm.set_value("delivery_contact_email", ''); + }, + pickup_from_send_shipping_notification: function(frm, cdt, cdn) { + if (frm.doc.pickup_contact_email && frm.doc.pickup_from_send_shipping_notification + && !validate_duplicate(frm, 'shipment_notification_subscriptions', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { + let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscriptions", "shipment_notification_subscriptions"); + row.email = frm.doc.pickup_contact_email; + frm.refresh_fields("shipment_notification_subscriptions"); + } + if (!frm.doc.pickup_from_send_shipping_notification) { + frm.events.remove_email_row(frm, 'shipment_notification_subscriptions', frm.doc.pickup_contact_email); + frm.refresh_fields("shipment_notification_subscriptions"); + } + }, + pickup_from_subscribe_to_status_updates: function(frm, cdt, cdn) { + if (frm.doc.pickup_contact_email && frm.doc.pickup_from_subscribe_to_status_updates + && !validate_duplicate(frm, 'shipment_status_update_subscriptions', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { + let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscriptions", "shipment_status_update_subscriptions"); + row.email = frm.doc.pickup_contact_email; + frm.refresh_fields("shipment_status_update_subscriptions"); + } + if (!frm.doc.pickup_from_subscribe_to_status_updates) { + frm.events.remove_email_row(frm, 'shipment_status_update_subscriptions', frm.doc.pickup_contact_email); + frm.refresh_fields("shipment_status_update_subscriptions"); + } + }, + delivery_to_send_shipping_notification: function(frm, cdt, cdn) { + if (frm.doc.delivery_contact_email && frm.doc.delivery_to_send_shipping_notification + && !validate_duplicate(frm, 'shipment_notification_subscriptions', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)){ + let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscriptions", "shipment_notification_subscriptions"); + row.email = frm.doc.delivery_contact_email; + frm.refresh_fields("shipment_notification_subscriptions"); + } + if (!frm.doc.delivery_to_send_shipping_notification) { + frm.events.remove_email_row(frm, 'shipment_notification_subscriptions', frm.doc.delivery_contact_email); + frm.refresh_fields("shipment_notification_subscriptions"); + } + }, + delivery_to_subscribe_to_status_updates: function(frm, cdt, cdn) { + if (frm.doc.delivery_contact_email && frm.doc.delivery_to_subscribe_to_status_updates + && !validate_duplicate(frm, 'shipment_status_update_subscriptions', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)) { + let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscriptions", "shipment_status_update_subscriptions"); + row.email = frm.doc.delivery_contact_email; + frm.refresh_fields("shipment_status_update_subscriptions"); + } + if (!frm.doc.delivery_to_subscribe_to_status_updates) { + frm.events.remove_email_row(frm, 'shipment_status_update_subscriptions', frm.doc.delivery_contact_email); + frm.refresh_fields("shipment_status_update_subscriptions"); + } + }, + remove_email_row: function(frm, table, fieldname) { + $.each(frm.doc[table] || [], function(i, detail) { + if(detail.email === fieldname){ + cur_frm.get_field(table).grid.grid_rows[i].remove(); + } + }); + }, + remove_notific_child_table: function(frm, table, delivery_type) { + $.each(frm.doc[table] || [], function(i, detail) { + if (detail.email != frm.doc.pickup_email || detail.email != frm.doc.delivery_email){ + cur_frm.get_field(table).grid.grid_rows[i].remove(); + } + }); + frm.refresh_fields(table); + if (delivery_type == 'Delivery') { + frm.set_value("delivery_to_send_shipping_notification", 0); + frm.set_value("delivery_to_subscribe_to_status_updates", 0); + frm.refresh_fields("delivery_to_send_shipping_notification"); + frm.refresh_fields("delivery_to_subscribe_to_status_updates"); + } + else { + frm.set_value("pickup_from_send_shipping_notification", 0); + frm.set_value("pickup_from_subscribe_to_status_updates", 0); + frm.refresh_fields("pickup_from_send_shipping_notification"); + frm.refresh_fields("pickup_from_subscribe_to_status_updates"); + } + }, + fetch_shipping_rates: function(frm) { + if (!frm.doc.shipment_id) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.fetch_shipping_rates", + freeze: true, + freeze_message: __("Fetching Shipping Rates"), + args: { + pickup_from_type: frm.doc.pickup_from_type, + delivery_to_type: frm.doc.delivery_to_type, + pickup_address_name: frm.doc.pickup_address_name, + delivery_address_name: frm.doc.delivery_address_name, + shipment_parcel: frm.doc.shipment_parcel, + description_of_content: frm.doc.description_of_content, + pickup_date: frm.doc.pickup_date, + pickup_contact_name: frm.doc.pickup_contact_name, + delivery_contact_name: frm.doc.delivery_contact_name, + value_of_goods: frm.doc.value_of_goods + }, + callback: function(r) { + if (r.message) { + select_from_available_services(frm, r.message); + } + else { + frappe.throw(__("No Shipment Services available")); + } + } + }); + } + else { + frappe.throw(__("Shipment already created")); + } + }, + print_shipping_label: function(frm) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.print_shipping_label", + freeze: true, + freeze_message: __("Printing Shipping Label"), + args: { + shipment_id: frm.doc.shipment_id, + service_provider: frm.doc.service_provider + }, + callback: function(r) { + if (r.message) { + if (frm.doc.service_provider == "LetMeShip") { + var array = JSON.parse(r.message); + // Uint8Array for unsigned bytes + array = new Uint8Array(array); + const file = new Blob([array], {type: "application/pdf"}); + const file_url = URL.createObjectURL(file); + window.open(file_url); + } + else { + if (Array.isArray(r.message)) { + r.message.forEach(url => window.open(url)); + } else { + window.open(r.message); + } + } + } + } + }); + }, + update_tracking: function(frm, service_provider, shipment_id) { + let delivery_notes = []; + (frm.doc.shipment_delivery_notes || []).forEach((d) => { + delivery_notes.push(d.delivery_note); + }); + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.update_tracking", + freeze: true, + freeze_message: __("Updating Tracking"), + args: { + shipment: frm.doc.name, + shipment_id: shipment_id, + service_provider: service_provider, + delivery_notes: delivery_notes + }, + callback: function(r) { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + } +}); + +frappe.ui.form.on('Shipment Delivery Notes', { + delivery_note: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.delivery_note) { + let row_index = row.idx - 1; + if(validate_duplicate(frm, 'shipment_delivery_notes', row.delivery_note, row_index)) { + cur_frm.get_field('shipment_delivery_notes').grid.grid_rows[row_index].remove(); + frappe.throw(__(`You have entered duplicate Delivery Notes. Please rectify and try again.`)); + } + } + }, + grand_total: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.grand_total) { + var value_of_goods = parseFloat(frm.doc.value_of_goods)+parseFloat(row.grand_total); + frm.set_value("value_of_goods", Math.round(value_of_goods)); + frm.refresh_fields("value_of_goods"); + } + }, +}); + +var validate_duplicate = function(frm, table, fieldname, index){ + let duplicate = false; + $.each(frm.doc[table], function(i, detail) { + // Email duplicate validation + if(detail.email === fieldname && !(index === i)) { + duplicate = true; + return; + } + + // Delivery Note duplicate validation + if(detail.delivery_note === fieldname && !(index === i)) { + duplicate = true; + return; + } + }); + return duplicate; +}; + +function select_from_available_services(frm, available_services) { + var headers = [ __("Service Provider"), __("Carrier"), __("Carrier’s Service"), __("Price"), "" ]; + cur_frm.render_available_services = function(d, headers, data){ + d.fields_dict.available_services.$wrapper.html( + frappe.render_template('shipment_service_selector', + {'header_columns': headers, 'data': data} + ) + ); + }; + const d = new frappe.ui.Dialog({ + title: __("Select Shipment Service to create Shipment"), + fields: [ + { + fieldtype:'HTML', + fieldname:"available_services", + label: __('Available Services') + } + ] + }); + cur_frm.render_available_services(d, headers, available_services); + let shipment_notific_email = []; + let tracking_notific_email = []; + (frm.doc.shipment_notification_subscriptions || []).forEach((d) => { + if (!d.unsubscribed) { + shipment_notific_email.push(d.email); + } + }); + (frm.doc.shipment_status_update_subscriptions || []).forEach((d) => { + if (!d.unsubscribed) { + tracking_notific_email.push(d.email); + } + }); + let delivery_notes = []; + (frm.doc.shipment_delivery_notes || []).forEach((d) => { + delivery_notes.push(d.delivery_note); + }); + cur_frm.select_row = function(service_data){ + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.create_shipment", + freeze: true, + freeze_message: __("Creating Shipment"), + args: { + shipment: frm.doc.name, + pickup_from_type: frm.doc.pickup_from_type, + delivery_to_type: frm.doc.delivery_to_type, + pickup_address_name: frm.doc.pickup_address_name, + delivery_address_name: frm.doc.delivery_address_name, + shipment_parcel: frm.doc.shipment_parcel, + description_of_content: frm.doc.description_of_content, + pickup_date: frm.doc.pickup_date, + pickup_contact_name: frm.doc.pickup_contact_name, + delivery_contact_name: frm.doc.delivery_contact_name, + value_of_goods: frm.doc.value_of_goods, + service_data: service_data, + shipment_notific_email: shipment_notific_email, + tracking_notific_email: tracking_notific_email, + delivery_notes: delivery_notes + }, + callback: function(r) { + if (!r.exc) { + frm.reload_doc(); + frappe.msgprint(__("Shipment created with {0}, ID is {1}", [r.message.service_provider, r.message.shipment_id])); + frm.events.update_tracking(frm, r.message.service_provider, r.message.shipment_id); + } + } + }); + d.hide(); + }; + d.show(); +} diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json new file mode 100644 index 0000000000..b6656a2b72 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -0,0 +1,478 @@ +{ + "actions": [], + "autoname": "SHIPMENT-.#####", + "creation": "2020-07-09 10:58:52.508703", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "heading_pickup_from", + "pickup_from_type", + "pickup_company", + "pickup_customer", + "pickup_supplier", + "pickup", + "pickup_address_name", + "pickup_address", + "pickup_contact_name", + "pickup_contact_email", + "pickup_contact", + "column_break_2", + "heading_delivery_to", + "delivery_to_type", + "delivery_company", + "delivery_customer", + "delivery_supplier", + "delivery_to", + "delivery_address_name", + "delivery_address", + "delivery_contact_name", + "delivery_contact_email", + "delivery_contact", + "notification_details_section", + "pickup_from_send_shipping_notification", + "pickup_from_subscribe_to_status_updates", + "shipment_notification_subscriptions", + "column_break_27", + "delivery_to_send_shipping_notification", + "delivery_to_subscribe_to_status_updates", + "shipment_status_update_subscriptions", + "parcels_section", + "shipment_parcel", + "parcel_template", + "add_template", + "column_break_28", + "shipment_delivery_notes", + "shipment_details_section", + "pallets", + "value_of_goods", + "pickup_date", + "pickup_from", + "pickup_to", + "column_break_36", + "shipment_type", + "pickup_type", + "incoterm", + "description_of_content", + "section_break_40", + "shipment_information_section", + "service_provider", + "shipment_id", + "shipment_amount", + "status", + "tracking_url", + "column_break_55", + "carrier", + "carrier_service", + "awb_number", + "tracking_status", + "tracking_status_info", + "amended_from" + ], + "fields": [ + { + "fieldname": "heading_pickup_from", + "fieldtype": "Heading", + "label": "Pickup from" + }, + { + "default": "Company", + "fieldname": "pickup_from_type", + "fieldtype": "Select", + "label": "Pickup from", + "options": "Company\nCustomer\nSupplier" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Company'", + "fieldname": "pickup_company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Customer'", + "fieldname": "pickup_customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.pickup_from_type == 'Supplier'", + "fieldname": "pickup_supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "pickup", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Pickup From", + "read_only": 1 + }, + { + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"", + "fieldname": "pickup_address_name", + "fieldtype": "Link", + "label": "Address", + "options": "Address", + "reqd": 1 + }, + { + "fieldname": "pickup_address", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"", + "fieldname": "pickup_contact_name", + "fieldtype": "Link", + "label": "Contact", + "options": "Contact" + }, + { + "fieldname": "pickup_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, + { + "fieldname": "pickup_contact", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "heading_delivery_to", + "fieldtype": "Heading", + "label": "Delivery to" + }, + { + "default": "Customer", + "fieldname": "delivery_to_type", + "fieldtype": "Select", + "label": "Delivery to", + "options": "Company\nCustomer\nSupplier" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Company'", + "fieldname": "delivery_company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Customer'", + "fieldname": "delivery_customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.delivery_to_type == 'Supplier'", + "fieldname": "delivery_supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "delivery_to", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Delivery To", + "read_only": 1 + }, + { + "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"", + "fieldname": "delivery_address_name", + "fieldtype": "Link", + "label": "Address", + "options": "Address", + "reqd": 1 + }, + { + "fieldname": "delivery_address", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "depends_on": "eval: doc.delivery_customer || doc.delivery_supplier || doc.delivery_to_type == \"Company\"", + "fieldname": "delivery_contact_name", + "fieldtype": "Link", + "label": "Contact", + "options": "Contact" + }, + { + "fieldname": "delivery_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "read_only": 1 + }, + { + "depends_on": "eval:doc.delivery_contact_name", + "fieldname": "delivery_contact", + "fieldtype": "Small Text", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "notification_details_section", + "fieldtype": "Section Break", + "label": "Notification Details" + }, + { + "default": "0", + "fieldname": "pickup_from_send_shipping_notification", + "fieldtype": "Check", + "label": "Send shipping notification" + }, + { + "default": "0", + "fieldname": "pickup_from_subscribe_to_status_updates", + "fieldtype": "Check", + "label": "Subscribe to status updates" + }, + { + "fieldname": "shipment_notification_subscriptions", + "fieldtype": "Table", + "label": "Shipment Notification Subscriptions", + "options": "Shipment Notification Subscriptions" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "delivery_to_send_shipping_notification", + "fieldtype": "Check", + "label": "Send shipping notification" + }, + { + "default": "0", + "fieldname": "delivery_to_subscribe_to_status_updates", + "fieldtype": "Check", + "label": "Subscribe to status updates" + }, + { + "fieldname": "shipment_status_update_subscriptions", + "fieldtype": "Table", + "label": "Shipment Status Update Subscriptions", + "options": "Shipment Status Update Subscriptions" + }, + { + "fieldname": "parcels_section", + "fieldtype": "Section Break", + "label": "Parcels" + }, + { + "fieldname": "shipment_parcel", + "fieldtype": "Table", + "label": "Shipment Parcel", + "options": "Shipment Parcel" + }, + { + "fieldname": "parcel_template", + "fieldtype": "Link", + "label": "Parcel Template", + "options": "Shipment Parcel Template" + }, + { + "fieldname": "add_template", + "fieldtype": "Button", + "label": "Add Template" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipment_delivery_notes", + "fieldtype": "Table", + "label": "Shipment Delivery Notes", + "options": "Shipment Delivery Notes" + }, + { + "fieldname": "shipment_details_section", + "fieldtype": "Section Break", + "label": "Shipment details" + }, + { + "default": "No", + "fieldname": "pallets", + "fieldtype": "Select", + "label": "Pallets", + "options": "No\nYes" + }, + { + "fieldname": "value_of_goods", + "fieldtype": "Currency", + "label": "Value of Goods", + "precision": "2", + "reqd": 1 + }, + { + "fieldname": "pickup_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Pickup Date", + "reqd": 1 + }, + { + "default": "09:00", + "fieldname": "pickup_from", + "fieldtype": "Select", + "label": "Pickup from", + "options": "09:00\n09:30\n10:00\n10:30\n11:00\n11:30\n12:00\n12:30\n13:00\n13:30\n14:00\n14:30\n15:00\n15:30\n16:00\n16:30\n17:00\n17:30\n18:00\n18:30\n19:00" + }, + { + "default": "17:00", + "fieldname": "pickup_to", + "fieldtype": "Select", + "label": "Pickup to", + "options": "09:00\n09:30\n10:00\n10:30\n11:00\n11:30\n12:00\n12:30\n13:00\n13:30\n14:00\n14:30\n15:00\n15:30\n16:00\n16:30\n17:00\n17:30\n18:00\n18:30\n19:00" + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "default": "Goods", + "fieldname": "shipment_type", + "fieldtype": "Select", + "label": "Shipment Type", + "options": "Goods\nDocuments" + }, + { + "default": "Pickup", + "fieldname": "pickup_type", + "fieldtype": "Select", + "label": "Pickup Type", + "options": "Pickup\nSelf delivery" + }, + { + "fieldname": "description_of_content", + "fieldtype": "Small Text", + "label": "Description of Content", + "reqd": 1 + }, + { + "fieldname": "section_break_40", + "fieldtype": "Section Break" + }, + { + "fieldname": "shipment_information_section", + "fieldtype": "Section Break", + "label": "Shipment Information" + }, + { + "fieldname": "service_provider", + "fieldtype": "Read Only", + "label": "Service Provider" + }, + { + "fieldname": "shipment_id", + "fieldtype": "Read Only", + "label": "Shipment ID" + }, + { + "fieldname": "shipment_amount", + "fieldtype": "Currency", + "label": "Shipment Amount", + "precision": "2", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", + "read_only": 1 + }, + { + "fieldname": "tracking_url", + "fieldtype": "Small Text", + "label": "Tracking URL", + "read_only": 1 + }, + { + "fieldname": "carrier", + "fieldtype": "Read Only", + "label": "Carrier" + }, + { + "fieldname": "carrier_service", + "fieldtype": "Read Only", + "label": "Carrier Service" + }, + { + "fieldname": "awb_number", + "fieldtype": "Read Only", + "label": "AWB Number" + }, + { + "fieldname": "tracking_status", + "fieldtype": "Select", + "label": "Tracking Status", + "options": "\nIn Progress\nDelivered\nReturned\nLost", + "read_only": 1 + }, + { + "fieldname": "tracking_status_info", + "fieldtype": "Data", + "label": "Tracking Status Info", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Shipment", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "incoterm", + "fieldtype": "Select", + "label": "Incoterm", + "options": "EXW (Ex Works)\nFCA (Free Carrier)\nCPT (Carriage Paid To)\nCIP (Carriage and Insurance Paid to)\nDPU (Delivered At Place Unloaded)\nDAP (Delivered At Place)\nDDP (Delivered Duty Paid)" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-07-24 11:44:30.904612", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py new file mode 100644 index 0000000000..e059bacfa1 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.model.document import Document +from erpnext.accounts.party import get_party_shipping_address +from frappe.contacts.doctype.contact.contact import get_default_contact +from erpnext.erpnext_integrations.doctype.letmeship.letmeship import LETMESHIP_PROVIDER, get_letmeship_available_services, create_letmeship_shipment, get_letmeship_label, get_letmeship_tracking_data +from erpnext.erpnext_integrations.doctype.packlink.packlink import PACKLINK_PROVIDER, get_packlink_available_services, create_packlink_shipment, get_packlink_label, get_packlink_tracking_data +from erpnext.erpnext_integrations.doctype.sendcloud.sendcloud import SENDCLOUD_PROVIDER, get_sendcloud_available_services, create_sendcloud_shipment, get_sendcloud_label, get_sendcloud_tracking_data +from erpnext.stock.doctype.parcel_service_type.parcel_service_type import match_parcel_service_type_alias + +class Shipment(Document): + def validate(self): + self.validate_weight() + if self.docstatus == 0: + self.status = 'Draft' + + def on_submit(self): + if not self.shipment_parcel: + frappe.throw(_('Please enter Shipment Parcel information')) + if self.value_of_goods == 0: + frappe.throw(_('Value of goods cannot be 0')) + self.status = 'Submitted' + + def on_cancel(self): + self.status = 'Cancelled' + + def validate_weight(self): + for parcel in self.shipment_parcel: + if parcel.weight <= 0: + frappe.throw(_('Parcel weight cannot be 0')) + +@frappe.whitelist() +def fetch_shipping_rates(pickup_from_type, delivery_to_type, pickup_address_name, delivery_address_name, + shipment_parcel, description_of_content, pickup_date, value_of_goods, + pickup_contact_name=None, delivery_contact_name=None): + # Return Shipping Rates for the various Shipping Providers + shipment_prices = [] + letmeship_enabled = frappe.db.get_single_value('LetMeShip','enabled') + packlink_enabled = frappe.db.get_single_value('Packlink','enabled') + sendcloud_enabled = frappe.db.get_single_value('SendCloud','enabled') + pickup_address = get_address(pickup_address_name) + delivery_address = get_address(delivery_address_name) + if letmeship_enabled: + pickup_contact = None + delivery_contact = None + if pickup_from_type != 'Company': + pickup_contact = get_contact(pickup_contact_name) + else: + pickup_contact = get_company_contact() + + if delivery_to_type != 'Company': + delivery_contact = get_contact(delivery_contact_name) + else: + delivery_contact = get_company_contact() + letmeship_prices = get_letmeship_available_services( + delivery_to_type=delivery_to_type, + pickup_address=pickup_address, + delivery_address=delivery_address, + shipment_parcel=shipment_parcel, + description_of_content=description_of_content, + pickup_date=pickup_date, + value_of_goods=value_of_goods, + pickup_contact=pickup_contact, + delivery_contact=delivery_contact, + ) + letmeship_prices = match_parcel_service_type_carrier(letmeship_prices, ['carrier', 'carrier_name']) + shipment_prices = shipment_prices + letmeship_prices + if packlink_enabled: + packlink_prices = get_packlink_available_services( + pickup_address=pickup_address, + delivery_address=delivery_address, + shipment_parcel=shipment_parcel, + pickup_date=pickup_date + ) + packlink_prices = match_parcel_service_type_carrier(packlink_prices, ['carrier_name', 'carrier']) + shipment_prices = shipment_prices + packlink_prices + if sendcloud_enabled and pickup_from_type == 'Company': + sendcloud_prices = get_sendcloud_available_services( + delivery_address=delivery_address, + shipment_parcel=shipment_parcel + ) + shipment_prices = shipment_prices + sendcloud_prices + shipment_prices = sorted(shipment_prices, key=lambda k:k['total_price']) + return shipment_prices + +@frappe.whitelist() +def create_shipment(shipment, pickup_from_type, delivery_to_type, pickup_address_name, + delivery_address_name, shipment_parcel, description_of_content, pickup_date, + value_of_goods, service_data, shipment_notific_email, tracking_notific_email, + pickup_contact_name=None, delivery_contact_name=None, delivery_notes=[]): + # Create Shipment for the selected provider + service_info = json.loads(service_data) + shipment_info = None + pickup_contact = None + delivery_contact = None + pickup_address = get_address(pickup_address_name) + delivery_address = get_address(delivery_address_name) + if pickup_from_type != 'Company': + pickup_contact = get_contact(pickup_contact_name) + else: + pickup_contact = get_company_contact() + + if delivery_to_type != 'Company': + delivery_contact = get_contact(delivery_contact_name) + else: + delivery_contact = get_company_contact() + if service_info['service_provider'] == LETMESHIP_PROVIDER: + shipment_info = create_letmeship_shipment( + pickup_address=pickup_address, + delivery_address=delivery_address, + shipment_parcel=shipment_parcel, + description_of_content=description_of_content, + pickup_date=pickup_date, + value_of_goods=value_of_goods, + pickup_contact=pickup_contact, + delivery_contact=delivery_contact, + service_info=service_info, + shipment_notific_email=shipment_notific_email, + tracking_notific_email=tracking_notific_email, + ) + + if service_info['service_provider'] == PACKLINK_PROVIDER: + shipment_info = create_packlink_shipment( + pickup_address=pickup_address, + delivery_address=delivery_address, + shipment_parcel=shipment_parcel, + description_of_content=description_of_content, + pickup_date=pickup_date, + value_of_goods=value_of_goods, + pickup_contact=pickup_contact, + delivery_contact=delivery_contact, + service_info=service_info, + ) + + if service_info['service_provider'] == SENDCLOUD_PROVIDER: + shipment_info = create_sendcloud_shipment( + shipment=shipment, + delivery_address=delivery_address, + shipment_parcel=shipment_parcel, + description_of_content=description_of_content, + value_of_goods=value_of_goods, + delivery_contact=delivery_contact, + service_info=service_info, + ) + + if shipment_info: + frappe.db.set_value('Shipment', shipment, 'service_provider', shipment_info.get('service_provider')) + frappe.db.set_value('Shipment', shipment, 'carrier', shipment_info.get('carrier')) + frappe.db.set_value('Shipment', shipment, 'carrier_service', shipment_info.get('carrier_service')) + frappe.db.set_value('Shipment', shipment, 'shipment_id', shipment_info.get('shipment_id')) + frappe.db.set_value('Shipment', shipment, 'shipment_amount', shipment_info.get('shipment_amount')) + frappe.db.set_value('Shipment', shipment, 'awb_number', shipment_info.get('awb_number')) + frappe.db.set_value('Shipment', shipment, 'status', 'Booked') + if delivery_notes: + update_delivery_note(delivery_notes=delivery_notes, shipment_info=shipment_info) + return shipment_info + + +@frappe.whitelist() +def print_shipping_label(service_provider, shipment_id): + if service_provider == LETMESHIP_PROVIDER: + shipping_label = get_letmeship_label(shipment_id) + elif service_provider == PACKLINK_PROVIDER: + shipping_label = get_packlink_label(shipment_id) + elif service_provider == SENDCLOUD_PROVIDER: + shipping_label = get_sendcloud_label(shipment_id) + return shipping_label + + +@frappe.whitelist() +def update_tracking(shipment, service_provider, shipment_id, delivery_notes=[]): + # Update Tracking info in Shipment + tracking_data = None + if service_provider == LETMESHIP_PROVIDER: + tracking_data = get_letmeship_tracking_data(shipment_id) + elif service_provider == PACKLINK_PROVIDER: + tracking_data = get_packlink_tracking_data(shipment_id) + elif service_provider == SENDCLOUD_PROVIDER: + tracking_data = get_sendcloud_tracking_data(shipment_id) + if tracking_data: + if delivery_notes: + update_delivery_note(delivery_notes=delivery_notes, tracking_info=tracking_data) + frappe.db.set_value('Shipment', shipment, 'awb_number', tracking_data.get('awb_number')) + frappe.db.set_value('Shipment', shipment, 'tracking_status', tracking_data.get('tracking_status')) + frappe.db.set_value('Shipment', shipment, 'tracking_status_info', tracking_data.get('tracking_status_info')) + frappe.db.set_value('Shipment', shipment, 'tracking_url', tracking_data.get('tracking_url')) + +@frappe.whitelist() +def get_address_name(ref_doctype, docname): + # Return address name + return get_party_shipping_address(ref_doctype, docname) + +@frappe.whitelist() +def get_contact_name(ref_doctype, docname): + # Return address name + return get_default_contact(ref_doctype, docname) + +def update_delivery_note(delivery_notes, shipment_info=None, tracking_info=None): + # Update Shipment Info in Delivery Note + # Using db_set since some services might not exist + for delivery_note in json.loads(delivery_notes): + dl_doc = frappe.get_doc('Delivery Note', delivery_note) + if shipment_info: + dl_doc.db_set('delivery_type', 'Parcel Service') + dl_doc.db_set('parcel_service', shipment_info.get('carrier')) + dl_doc.db_set('parcel_service_type', shipment_info.get('carrier_service')) + if tracking_info: + dl_doc.db_set('tracking_number', tracking_info.get('awb_number')) + dl_doc.db_set('tracking_url', tracking_info.get('tracking_url')) + dl_doc.db_set('tracking_status', tracking_info.get('tracking_status')) + dl_doc.db_set('tracking_status_info', tracking_info.get('tracking_status_info')) + + +def update_tracking_info(): + # Daily scheduled event to update Tracking info for not delivered Shipments + # Also Updates the related Delivery Notes + shipments = frappe.get_all('Shipment', filters={ + 'docstatus': 1, + 'status': 'Booked', + 'shipment_id': ['!=', ''], + 'tracking_status': ['!=', 'Delivered'], + }) + for shipment in shipments: + shipment_doc = frappe.get_doc('Shipment', shipment.name) + tracking_info = \ + update_tracking( + shipment_doc.service_provider, + shipment_doc.shipment_id, + shipment_doc.shipment_delivery_notes + ) + if tracking_info: + shipment_doc.db_set('awb_number', tracking_info.get('awb_number')) + shipment_doc.db_set('tracking_url', tracking_info.get('tracking_url')) + shipment_doc.db_set('tracking_status', tracking_info.get('tracking_status')) + shipment_doc.db_set('tracking_status_info', tracking_info.get('tracking_status_info')) + + +def get_address(address_name): + address = frappe.db.get_value('Address', address_name, [ + 'address_title', + 'address_line1', + 'address_line2', + 'city', + 'pincode', + 'country', + ], as_dict=1) + address.country_code = frappe.db.get_value('Country', address.country, 'code').upper() + if not address.pincode or address.pincode == '': + frappe.throw(_("Postal Code is mandatory to continue.
\ + Please set Postal Code for Address {1}" + ).format(address_name, address_name)) + address.pincode = address.pincode.replace(' ', '') + address.city = address.city.strip() + return address + + +def get_contact(contact_name): + contact = frappe.db.get_value('Contact', contact_name, [ + 'first_name', + 'last_name', + 'email_id', + 'phone', + 'mobile_no', + 'gender', + ], as_dict=1) + if not contact.last_name: + frappe.throw(_("Last Name is mandatory to continue.
\ + Please set Last Name for Contact {1}" + ).format(contact_name, contact_name)) + if not contact.phone: + contact.phone = contact.mobile_no + return contact + + +def get_company_contact(): + contact = frappe.db.get_value('User', frappe.session.user, [ + 'first_name', + 'last_name', + 'email', + 'phone', + 'mobile_no', + 'gender', + ], as_dict=1) + if not contact.phone: + contact.phone = contact.mobile_no + return contact + +def match_parcel_service_type_carrier(shipment_prices, reference): + for idx, prices in enumerate(shipment_prices): + service_name = match_parcel_service_type_alias(prices.get(reference[0]), prices.get(reference[1])) + is_preferred = frappe.db.get_value('Parcel Service Type', service_name, 'show_in_preferred_services_list') + shipment_prices[idx].service_name = service_name + shipment_prices[idx].is_preferred = is_preferred + return shipment_prices diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js new file mode 100644 index 0000000000..57e92099cb --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings['Shipment'] = { + add_fields: ["status"], + get_indicator: function(doc) { + if(doc.status=='Booked') { + return [__("Booked"), "green"]; + } + } +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/shipment_service_selector.html b/erpnext/stock/doctype/shipment/shipment_service_selector.html new file mode 100644 index 0000000000..ed9b8bf400 --- /dev/null +++ b/erpnext/stock/doctype/shipment/shipment_service_selector.html @@ -0,0 +1,70 @@ +{% if (data.length) { %} +
+
{{ __("Preferred Services") }}
+ + + + {% for (var i = 0; i < header_columns.length; i++) { %} + + {% } %} + + + + {% for (var i = 0; i < data.length; i++) { %} + {% if (data[i].is_preferred) { %} + + + + + + + + {% } %} + {% } %} + +
{{ header_columns[i] }}
{{ data[i].service_provider }}{{ data[i].carrier }}{{ data[i].service_name }}{{ format_currency(data[i].total_price, 'EUR', 2) }} + +
+
{{ __("Other Services") }}
+ + + + {% for (var i = 0; i < header_columns.length; i++) { %} + + {% } %} + + + + {% for (var i = 0; i < data.length; i++) { %} + {% if (!data[i].is_preferred) { %} + + + + + + + + {% } %} + {% } %} + +
{{ header_columns[i] }}
{{ data[i].service_provider }}{{ data[i].carrier }}{{ data[i].service_name }}{{ format_currency(data[i].total_price, 'EUR', 2) }} + +
+
+{% } else { %} +
{{ __("No Services Available") }}
+{% } %} + + \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py new file mode 100644 index 0000000000..6a06930e82 --- /dev/null +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import json +from datetime import date, timedelta + +import frappe +import unittest +from erpnext.stock.doctype.shipment.shipment import fetch_shipping_rates +from erpnext.stock.doctype.shipment.shipment import create_shipment +from erpnext.stock.doctype.shipment.shipment import update_tracking + +class TestShipment(unittest.TestCase): + pass + + def test_shipment_booking(self): + shipment = create_test_shipment() + try: + shipment.submit() + except: + frappe.throw('Error occurred on submit shipment') + doc, rate, tracking_data = make_shipment_transaction(shipment) + if doc and rate and tracking_data: + self.assertEqual(doc.service_provider, rate.get('service_provider')) + self.assertEqual(doc.shipment_amount, rate.get('actual_price')) + self.assertEqual(doc.carrier, rate.get('carrier')) + self.assertEqual(doc.tracking_status, tracking_data.get('tracking_status')) + self.assertEqual(doc.tracking_url, tracking_data.get('tracking_url')) + + def test_shipment_from_delivery_note(self): + delivery_note = create_test_delivery_note() + try: + delivery_note.submit() + except: + frappe.throw('An error occurred.') + + shipment = create_test_shipment([ delivery_note ]) + try: + shipment.submit() + except: + frappe.throw('Error occurred on submit shipment') + doc, rate, tracking_data = make_shipment_transaction(shipment) + if doc and rate and tracking_data: + self.assertEqual(doc.service_provider, rate.get('service_provider')) + self.assertEqual(doc.shipment_amount, rate.get('actual_price')) + self.assertEqual(doc.carrier, rate.get('carrier')) + self.assertEqual(doc.tracking_status, tracking_data.get('tracking_status')) + self.assertEqual(doc.tracking_url, tracking_data.get('tracking_url')) + + + +def make_shipment_transaction(shipment): + shipment_parcel = convert_shipmet_parcel(shipment.shipment_parcel) + shipment_rates = fetch_shipping_rates(shipment.pickup_from_type, shipment.delivery_to_type, + shipment.pickup_address_name, shipment.delivery_address_name, + shipment_parcel, shipment.description_of_content, + shipment.pickup_date, shipment.value_of_goods, + pickup_contact_name=shipment.pickup_contact_name, + delivery_contact_name=shipment.delivery_contact_name + ) + if len(shipment_rates) > 0: + # We are taking the first shipment rate + rate = shipment_rates[0] + new_shipment = create_shipment( + shipment=shipment.name, + pickup_from_type=shipment.pickup_from_type, + delivery_to_type=shipment.delivery_to_type, + pickup_address_name=shipment.pickup_address_name, + delivery_address_name=shipment.delivery_address_name, + shipment_parcel=shipment_parcel, + description_of_content=shipment.description_of_content, + pickup_date=shipment.pickup_date, + pickup_contact_name=shipment.pickup_contact_name, + delivery_contact_name=shipment.delivery_contact_name, + value_of_goods=shipment.value_of_goods, + service_data=json.dumps(rate), + shipment_notific_email=None, + tracking_notific_email=None, + delivery_notes=None + ) + service_provider = rate.get('service_provider') + shipment_id = new_shipment.get('shipment_id') + tracking_data = update_tracking( + shipment.name, + service_provider, + shipment_id, + delivery_notes=None + ) + doc = frappe.get_doc('Shipment', shipment.name) + return doc, rate, tracking_data + return None, None, None + +def create_test_delivery_note(): + company = get_shipment_company() + customer = get_shipment_customer() + item = get_shipment_item(company.name) + posting_date = date.today() + timedelta(days=1) + + create_material_receipt(item, company.name) + delivery_note = frappe.new_doc("Delivery Note") + delivery_note.company = company.name + delivery_note.posting_date = posting_date.strftime("%Y-%m-%d") + delivery_note.posting_time = '10:00' + delivery_note.customer = customer.name + delivery_note.append('items', + { + "item_code": item.name, + "item_name": item.item_name, + "description": 'Test delivery note for shipment', + "qty": 5, + "uom": 'Nos', + "warehouse": 'Stores - SC', + "rate": item.standard_rate, + "cost_center": 'Main - SC' + } + ) + delivery_note.insert() + frappe.db.commit() + return delivery_note + + +def create_test_shipment(delivery_notes=[]): + company = get_shipment_company() + company_address = get_shipment_company_address(company.name) + customer = get_shipment_customer() + customer_address = get_shipment_customer_address(customer.name) + customer_contact = get_shipment_customer_contact(customer.name) + posting_date = date.today() + timedelta(days=5) + + shipment = frappe.new_doc("Shipment") + shipment.pickup_from_type = 'Company' + shipment.pickup_company = company.name + shipment.pickup_address_name = company_address.name + shipment.delivery_to_type = 'Customer' + shipment.delivery_customer = customer.name + shipment.delivery_address_name = customer_address.name + shipment.delivery_contact_name = customer_contact.name + shipment.pallets = 'No' + shipment.shipment_type = 'Goods' + shipment.value_of_goods = 1000 + shipment.pickup_type = 'Pickup' + shipment.pickup_date = posting_date.strftime("%Y-%m-%d") + shipment.pickup_from = '09:00' + shipment.pickup_to = '17:00' + shipment.description_of_content = 'unit test entry' + for delivery_note in delivery_notes: + shipment.append('shipment_delivery_notes', + { + "delivery_note": delivery_note.name + } + ) + shipment.append('shipment_parcel', + { + "length": 5, + "width": 5, + "height": 5, + "weight": 5, + "count": 5 + } + ) + shipment.insert() + frappe.db.commit() + return shipment + + +def get_shipment_customer_contact(customer_name): + contact_fname = 'Customer Shipment' + contact_lname = 'Testing' + customer_name = contact_fname + ' ' + contact_lname + contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name}) + if len(contacts): + return contacts[0] + else: + return create_customer_contact(contact_fname, contact_lname) + + +def get_shipment_customer_address(customer_name): + address_title = customer_name + ' address 123' + customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + if len(customer_address): + return customer_address[0] + else: + return create_shipment_address(address_title, customer_name, 81929) + +def get_shipment_customer(): + customer_name = 'Shipment Customer' + customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name}) + if len(customer): + return customer[0] + else: + return create_shipment_customer(customer_name) + +def get_shipment_company_address(company_name): + address_title = company_name + ' address 123' + addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + if len(addresses): + return addresses[0] + else: + return create_shipment_address(address_title, company_name, 80331) + +def get_shipment_company(): + company_name = 'Shipment Company' + abbr = 'SC' + companies = frappe.get_all("Company", fields=["name"], filters = {"company_name": company_name}) + if len(companies): + return companies[0] + else: + return create_shipment_company(company_name, abbr) + +def get_shipment_item(company_name): + item_name = 'Testing Shipment item' + items = frappe.get_all("Item", + fields=["name", "item_name", "item_code", "standard_rate"], + filters = {"item_name": item_name} + ) + if len(items): + return items[0] + else: + return create_shipment_item(item_name, company_name) + +def create_shipment_address(address_title, company_name, postal_code): + address = frappe.new_doc("Address") + address.address_title = address_title + address.address_type = 'Shipping' + address.address_line1 = company_name + ' address line 1' + address.city = 'Random City' + address.postal_code = postal_code + address.country = 'Germany' + address.insert() + return address + + +def create_customer_contact(fname, lname): + customer = frappe.new_doc("Contact") + customer.customer_name = fname + ' ' + lname + customer.first_name = fname + customer.last_name = lname + customer.is_primary_contact = 1 + customer.is_billing_contact = 1 + customer.append('email_ids', + { + 'email_id': 'randomme@email.com', + 'is_primary': 1 + } + ) + customer.append('phone_nos', + { + 'phone': '123123123', + 'is_primary_phone': 1, + 'is_primary_mobile_no': 1 + } + ) + customer.status = 'Passive' + customer.insert() + return customer + + +def create_shipment_company(company_name, abbr): + company = frappe.new_doc("Company") + company.company_name = company_name + company.abbr = abbr + company.default_currency = 'EUR' + company.country = 'Germany' + company.insert() + return company + +def create_shipment_customer(customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.customer_type = 'Company' + customer.customer_group = 'All Customer Groups' + customer.territory = 'All Territories' + customer.gst_category = 'Unregistered' + customer.insert() + return customer + +def create_material_receipt(item, company): + posting_date = date.today() + stock = frappe.new_doc("Stock Entry") + stock.company = company + stock.stock_entry_type = 'Material Receipt' + stock.posting_date = posting_date.strftime("%Y-%m-%d") + stock.append('items', + { + "t_warehouse": 'Stores - SC', + "item_code": item.name, + "qty": 5, + "uom": 'Nos', + "basic_rate": item.standard_rate, + "cost_center": 'Main - SC' + } + ) + stock.insert() + try: + stock.submit() + except: + frappe.throw('An error occurred.') + + +def create_shipment_item(item_name, company_name): + item = frappe.new_doc("Item") + item.item_name = item_name + item.item_code = item_name + item.item_group = 'All Item Groups' + item.opening_stock = 'Nos' + item.standard_rate = 50 + item.append('item_defaults', + { + "company": company_name, + "default_warehouse": 'Stores - SC' + } + ) + try: + item.insert() + except: + frappe.throw('An error occurred.') + return item + + +def convert_shipmet_parcel(shipmet_parcel): + data = [] + for parcel in shipmet_parcel: + data.append( + { + "length": parcel.length, + "width": parcel.width, + "height": parcel.height, + "weight": parcel.weight, + "count": parcel.count + } + ) + return json.dumps(data) diff --git a/erpnext/stock/doctype/shipment_delivery_notes/__init__.py b/erpnext/stock/doctype/shipment_delivery_notes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json new file mode 100644 index 0000000000..fbc01d9a24 --- /dev/null +++ b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json @@ -0,0 +1,41 @@ +{ + "actions": [], + "creation": "2020-07-09 11:52:57.939021", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "delivery_note", + "grand_total" + ], + "fields": [ + { + "fieldname": "delivery_note", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Delivery Note", + "options": "Delivery Note", + "reqd": 1 + }, + { + "fetch_from": "delivery_note.grand_total", + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Value", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-09 12:55:01.134270", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Delivery Notes", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py new file mode 100644 index 0000000000..ed936c60f8 --- /dev/null +++ b/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentDeliveryNotes(Document): + pass diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py b/erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json new file mode 100644 index 0000000000..bd9b8003a8 --- /dev/null +++ b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2020-07-09 12:49:09.185552", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "email", + "unsubscribed" + ], + "fields": [ + { + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "email", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "in_list_view": 1, + "label": "unsubscribed" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-09 12:55:14.217387", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Notification Subscriptions", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py new file mode 100644 index 0000000000..28ead7fab8 --- /dev/null +++ b/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentNotificationSubscriptions(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel/__init__.py b/erpnext/stock/doctype/shipment_parcel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json new file mode 100644 index 0000000000..6943edcdc9 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "creation": "2020-07-09 11:28:48.887737", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "length", + "width", + "height", + "weight", + "count" + ], + "fields": [ + { + "fieldname": "length", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Length (cm)", + "reqd": 1 + }, + { + "fieldname": "width", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Width (cm)", + "reqd": 1 + }, + { + "fieldname": "height", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Height (cm)", + "reqd": 1 + }, + { + "fieldname": "weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Weight (kg)", + "precision": "1", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "count", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Count", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-09 12:54:14.847170", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Parcel", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py new file mode 100644 index 0000000000..53e6ed55dd --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel/shipment_parcel.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentParcel(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/__init__.py b/erpnext/stock/doctype/shipment_parcel_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js new file mode 100644 index 0000000000..785a3b304d --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Shipment Parcel Template', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json new file mode 100644 index 0000000000..ec2bb1c9b3 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "autoname": "field:preset_name", + "creation": "2020-07-09 11:43:43.470339", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "preset_name", + "length", + "width", + "height", + "weight" + ], + "fields": [ + { + "fieldname": "preset_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Preset Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "length", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Length (cm)", + "reqd": 1 + }, + { + "fieldname": "width", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Width (cm)", + "reqd": 1 + }, + { + "fieldname": "height", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Height (cm)", + "reqd": 1 + }, + { + "fieldname": "weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Weight (kg)", + "precision": "1", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-07-10 12:53:22.772826", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Parcel Template", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py new file mode 100644 index 0000000000..2a8d58d830 --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentParcelTemplate(Document): + pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py new file mode 100644 index 0000000000..6e2caa768b --- /dev/null +++ b/erpnext/stock/doctype/shipment_parcel_template/test_shipment_parcel_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestShipmentParcelTemplate(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py b/erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json new file mode 100644 index 0000000000..3b86b400d8 --- /dev/null +++ b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2020-07-09 12:51:10.656612", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "email", + "unsubscribed" + ], + "fields": [ + { + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "email", + "reqd": 1, + "unique": 1 + }, + { + "default": "0", + "fieldname": "unsubscribed", + "fieldtype": "Check", + "in_list_view": 1, + "label": "unsubscribed" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-09 12:55:27.615463", + "modified_by": "Administrator", + "module": "Stock", + "name": "Shipment Status Update Subscriptions", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py new file mode 100644 index 0000000000..a8e31ea778 --- /dev/null +++ b/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ShipmentStatusUpdateSubscriptions(Document): + pass From 000494f54856fd8abedd71c44c173bd4698bd8f8 Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Thu, 1 Oct 2020 01:23:29 +0000 Subject: [PATCH 216/229] fix(shipment): apply code review --- .../doctype/letmeship/letmeship.json | 4 +- .../doctype/letmeship/letmeship.py | 262 +++++++++--------- .../doctype/packlink/packlink.py | 3 + .../doctype/sendcloud/sendcloud.json | 4 +- .../doctype/sendcloud/sendcloud.py | 19 +- erpnext/erpnext_integrations/utils.py | 2 - .../doctype/delivery_note/delivery_note.py | 28 +- erpnext/stock/doctype/shipment/api/utils.py | 67 ----- erpnext/stock/doctype/shipment/shipment.js | 203 ++++++++------ erpnext/stock/doctype/shipment/shipment.json | 93 ++++--- erpnext/stock/doctype/shipment/shipment.py | 42 ++- .../shipment/shipment_service_selector.html | 88 +++--- .../__init__.py | 0 .../shipment_delivery_note.json} | 2 +- .../shipment_delivery_note.py} | 2 +- .../__init__.py | 0 .../shipment_notification_subscription.json} | 2 +- .../shipment_notification_subscription.py} | 2 +- .../shipment_parcel_template.json | 22 +- .../__init__.py | 0 .../shipment_status_update_subscription.json} | 2 +- .../shipment_status_update_subscription.py} | 2 +- 22 files changed, 405 insertions(+), 444 deletions(-) delete mode 100644 erpnext/stock/doctype/shipment/api/utils.py rename erpnext/stock/doctype/{shipment_delivery_notes => shipment_delivery_note}/__init__.py (100%) rename erpnext/stock/doctype/{shipment_delivery_notes/shipment_delivery_notes.json => shipment_delivery_note/shipment_delivery_note.json} (95%) rename erpnext/stock/doctype/{shipment_delivery_notes/shipment_delivery_notes.py => shipment_delivery_note/shipment_delivery_note.py} (86%) rename erpnext/stock/doctype/{shipment_notification_subscriptions => shipment_notification_subscription}/__init__.py (100%) rename erpnext/stock/doctype/{shipment_notification_subscriptions/shipment_notification_subscriptions.json => shipment_notification_subscription/shipment_notification_subscription.json} (93%) rename erpnext/stock/doctype/{shipment_notification_subscriptions/shipment_notification_subscriptions.py => shipment_notification_subscription/shipment_notification_subscription.py} (83%) rename erpnext/stock/doctype/{shipment_status_update_subscriptions => shipment_status_update_subscription}/__init__.py (100%) rename erpnext/stock/doctype/{shipment_status_update_subscriptions/shipment_status_update_subscriptions.json => shipment_status_update_subscription/shipment_status_update_subscription.json} (93%) rename erpnext/stock/doctype/{shipment_status_update_subscriptions/shipment_status_update_subscriptions.py => shipment_status_update_subscription/shipment_status_update_subscription.py} (83%) diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json index 4a9a70f251..94b001ed08 100644 --- a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json +++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json @@ -24,14 +24,14 @@ }, { "fieldname": "api_password", - "fieldtype": "Data", + "fieldtype": "Password", "label": "API Password", "read_only_depends_on": "eval:doc.enabled == 0" } ], "issingle": 1, "links": [], - "modified": "2020-08-05 16:33:44.548230", + "modified": "2020-10-21 10:28:37.607717", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "LetMeShip", diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py index 3ad06dbb58..162c1edb37 100644 --- a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py +++ b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py @@ -20,9 +20,7 @@ def get_letmeship_available_services(delivery_to_type, pickup_address, delivery_address, shipment_parcel, description_of_content, pickup_date, value_of_goods, pickup_contact=None, delivery_contact=None): # Retrieve rates at LetMeShip from specification stated. - enabled = frappe.db.get_single_value('LetMeShip','enabled') - api_id = frappe.db.get_single_value('LetMeShip','api_id') - api_password = frappe.db.get_single_value('LetMeShip','api_password') + api_id, api_password, enabled = frappe.db.get_value('LetMeShip', 'LetMeShip', ['enabled', 'api_password', 'api_id']) if not enabled or not api_id or not api_password: return [] @@ -41,62 +39,16 @@ def get_letmeship_available_services(delivery_to_type, pickup_address, 'Accept': 'application/json', 'Access-Control-Allow-Origin': 'string' } - payload = {'pickupInfo': { - 'address': { - 'countryCode': pickup_address.country_code, - 'zip': pickup_address.pincode, - 'city': pickup_address.city, - 'street': pickup_address.address_line1, - 'addressInfo1': pickup_address.address_line2, - 'houseNo': '', - }, - 'company': pickup_address.address_title, - 'person': { - 'title': pickup_contact.title, - 'firstname': pickup_contact.first_name, - 'lastname': pickup_contact.last_name - }, - 'phone': { - 'phoneNumber': pickup_contact.phone, - 'phoneNumberPrefix': pickup_contact.phone_prefix - }, - 'email': pickup_contact.email, - }, 'deliveryInfo': { - 'address': { - 'countryCode': delivery_address.country_code, - 'zip': delivery_address.pincode, - 'city': delivery_address.city, - 'street': delivery_address.address_line1, - 'addressInfo1': delivery_address.address_line2, - 'houseNo': '', - }, - 'company': delivery_address.address_title, - 'person': { - 'title': delivery_contact.title, - 'firstname': delivery_contact.first_name, - 'lastname': delivery_contact.last_name - }, - 'phone': { - 'phoneNumber': delivery_contact.phone, - 'phoneNumberPrefix': delivery_contact.phone_prefix - }, - 'email': delivery_contact.email, - }, 'shipmentDetails': { - 'contentDescription': description_of_content, - 'shipmentType': 'PARCEL', - 'shipmentSettings': { - 'saturdayDelivery': False, - 'ddp': False, - 'insurance': False, - 'pickupOrder': False, - 'pickupTailLift': False, - 'deliveryTailLift': False, - 'holidayDelivery': False, - }, - 'goodsValue': value_of_goods, - 'parcelList': parcel_list, - 'pickupInterval': {'date': pickup_date}, - }} + payload = generate_payload( + pickup_address=pickup_address, + pickup_contact=pickup_contact, + delivery_address=delivery_address, + delivery_contact=delivery_contact, + description_of_content=description_of_content, + value_of_goods=value_of_goods, + parcel_list=parcel_list, + pickup_date=pickup_date + ) try: available_services = [] response_data = requests.post( @@ -128,6 +80,7 @@ def get_letmeship_available_services(delivery_to_type, pickup_address, .format(response_data['message']) ) except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint( _('Error occurred while fetching LetMeShip Prices: {0}') .format(str(exc)), @@ -142,9 +95,7 @@ def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, pickup_contact=None, delivery_contact=None): # Create a transaction at LetMeShip # LetMeShip have limit of 30 characters for Company field - enabled = frappe.db.get_single_value('LetMeShip','enabled') - api_id = frappe.db.get_single_value('LetMeShip','api_id') - api_password = frappe.db.get_single_value('LetMeShip','api_password') + api_id, api_password, enabled = frappe.db.get_value('LetMeShip', 'LetMeShip', ['enabled', 'api_password', 'api_id']) if not enabled or not api_id or not api_password: return [] @@ -162,6 +113,72 @@ def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, 'Accept': 'application/json', 'Access-Control-Allow-Origin': 'string' } + payload = generate_payload( + pickup_address=pickup_address, + pickup_contact=pickup_contact, + delivery_address=delivery_address, + delivery_contact=delivery_contact, + description_of_content=description_of_content, + value_of_goods=value_of_goods, + parcel_list=parcel_list, + pickup_date=pickup_date, + service_info=service_info, + tracking_notific_email=tracking_notific_email, + shipment_notific_email=shipment_notific_email + ) + try: + response_data = requests.post( + url=url, + auth=(api_id, api_password), + headers=headers, + data=json.dumps(payload) + ) + response_data = json.loads(response_data.text) + if 'shipmentId' in response_data: + shipment_amount = response_data['service']['priceInfo']['totalPrice'] + awb_number = '' + url = 'https://api.letmeship.com/v1/shipments/{id}'.format(id=response_data['shipmentId']) + tracking_response = requests.get(url, auth=(api_id, api_password),headers=headers) + tracking_response_data = json.loads(tracking_response.text) + if 'trackingData' in tracking_response_data: + for parcel in tracking_response_data['trackingData']['parcelList']: + if 'awbNumber' in parcel: + awb_number = parcel['awbNumber'] + return { + 'service_provider': LETMESHIP_PROVIDER, + 'shipment_id': response_data['shipmentId'], + 'carrier': service_info['carrier'], + 'carrier_service': service_info['service_name'], + 'shipment_amount': shipment_amount, + 'awb_number': awb_number, + } + elif 'message' in response_data: + frappe.throw( + _('Error occurred while creating Shipment: {0}') + .format(response_data['message']) + ) + except Exception as exc: + frappe.log_error(frappe.get_traceback()) + frappe.msgprint( + _('Error occurred while creating Shipment: {0}') + .format(str(exc)), + indicator='orange', + alert=True + ) + +def generate_payload( + pickup_address, + pickup_contact, + delivery_address, + delivery_contact, + description_of_content, + value_of_goods, + parcel_list, + pickup_date, + service_info=None, + tracking_notific_email=None, + shipment_notific_email=None +): payload = { 'pickupInfo': { 'address': { @@ -205,18 +222,6 @@ def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, }, 'email': delivery_contact.email, }, - 'service': { - 'baseServiceDetails': { - 'id': service_info['id'], - 'name': service_info['service_name'], - 'carrier': service_info['carrier'], - 'priceInfo': service_info['price_info'], - }, - 'supportedExWorkType': [], - 'messages': [''], - 'description': '', - 'serviceInfo': '', - }, 'shipmentDetails': { 'contentDescription': description_of_content, 'shipmentType': 'PARCEL', @@ -233,10 +238,24 @@ def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, 'parcelList': parcel_list, 'pickupInterval': { 'date': pickup_date + } + } + } + + if service_info: + payload['service'] = { + 'baseServiceDetails': { + 'id': service_info['id'], + 'name': service_info['service_name'], + 'carrier': service_info['carrier'], + 'priceInfo': service_info['price_info'], }, - 'contentDescription': description_of_content, - }, - 'shipmentNotification': { + 'supportedExWorkType': [], + 'messages': [''], + 'description': '', + 'serviceInfo': '', + } + payload['shipmentNotification'] = { 'trackingNotification': { 'deliveryNotification': True, 'problemNotification': True, @@ -247,77 +266,47 @@ def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, 'notificationText': '', 'emails': [ shipment_notific_email ] } - }, - 'labelEmail': True, - } + } + payload['labelEmail'] = True + return payload + +def get_letmeship_label(shipment_id): try: - response_data = requests.post( - url=url, - auth=(api_id, api_password), - headers=headers, - data=json.dumps(payload) + # Retrieve shipment label from LetMeShip + api_id = frappe.db.get_single_value('LetMeShip','api_id') + api_password = frappe.db.get_single_value('LetMeShip','api_password') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Access-Control-Allow-Origin': 'string' + } + url = 'https://api.letmeship.com/v1/shipments/{id}/documents?types=LABEL'\ + .format(id=shipment_id) + shipment_label_response = requests.get( + url, + auth=(api_id,api_password), + headers=headers ) - response_data = json.loads(response_data.text) - if 'shipmentId' in response_data: - shipment_amount = response_data['service']['priceInfo']['totalPrice'] - awb_number = '' - url = 'https://api.letmeship.com/v1/shipments/{id}'.format(id=response_data['shipmentId']) - tracking_response = requests.get(url, auth=(api_id, api_password),headers=headers) - tracking_response_data = json.loads(tracking_response.text) - if 'trackingData' in tracking_response_data: - for parcel in tracking_response_data['trackingData']['parcelList']: - if 'awbNumber' in parcel: - awb_number = parcel['awbNumber'] - return { - 'service_provider': LETMESHIP_PROVIDER, - 'shipment_id': response_data['shipmentId'], - 'carrier': service_info['carrier'], - 'carrier_service': service_info['service_name'], - 'shipment_amount': shipment_amount, - 'awb_number': awb_number, - } - elif 'message' in response_data: + shipment_label_response_data = json.loads(shipment_label_response.text) + if 'documents' in shipment_label_response_data: + for label in shipment_label_response_data['documents']: + if 'data' in label: + return json.dumps(label['data']) + else: frappe.throw( - _('Error occurred while creating Shipment: {0}') - .format(response_data['message']) + _('Error occurred while printing Shipment: {0}') + .format(shipment_label_response_data['message']) ) except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint( - _('Error occurred while creating Shipment: {0}') + _('Error occurred while printing Shipment: {0}') .format(str(exc)), indicator='orange', alert=True ) -def get_letmeship_label(shipment_id): - # Retrieve shipment label from LetMeShip - api_id = frappe.db.get_single_value('LetMeShip','api_id') - api_password = frappe.db.get_single_value('LetMeShip','api_password') - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Access-Control-Allow-Origin': 'string' - } - url = 'https://api.letmeship.com/v1/shipments/{id}/documents?types=LABEL'\ - .format(id=shipment_id) - shipment_label_response = requests.get( - url, - auth=(api_id,api_password), - headers=headers - ) - shipment_label_response_data = json.loads(shipment_label_response.text) - if 'documents' in shipment_label_response_data: - for label in shipment_label_response_data['documents']: - if 'data' in label: - return json.dumps(label['data']) - else: - frappe.throw( - _('Error occurred while printing Shipment: {0}') - .format(shipment_label_response_data['message']) - ) - - def get_letmeship_tracking_data(shipment_id): # return letmeship tracking data api_id = frappe.db.get_single_value('LetMeShip','api_id') @@ -359,6 +348,7 @@ def get_letmeship_tracking_data(shipment_id): .format(tracking_data['message']) ) except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint( _('Error occurred while updating Shipment: {0}') .format(str(exc)), diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.py b/erpnext/erpnext_integrations/doctype/packlink/packlink.py index 7fdb053cf8..1db08c3149 100644 --- a/erpnext/erpnext_integrations/doctype/packlink/packlink.py +++ b/erpnext/erpnext_integrations/doctype/packlink/packlink.py @@ -72,6 +72,7 @@ def get_packlink_available_services(pickup_address, delivery_address, shipment_p return available_services except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint( _('Error occurred on Packlink: {0}') .format(str(exc)), indicator='orange', @@ -152,6 +153,7 @@ def create_packlink_shipment(pickup_address, delivery_address, shipment_parcel, 'awb_number': '', } except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint( _('Error occurred while creating Shipment: {0}') .format(str(exc)), @@ -215,6 +217,7 @@ def get_packlink_tracking_data(shipment_id): 'tracking_url': tracking_url } except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint(_('Error occurred while updating Shipment: {0}').format( str(exc)), indicator='orange', alert=True) return [] diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json index dab54cba6c..37b6898cba 100644 --- a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json +++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json @@ -24,7 +24,7 @@ }, { "fieldname": "api_secret", - "fieldtype": "Data", + "fieldtype": "Password", "label": "API Secret", "read_only_depends_on": "eval:doc.enabled == 0" } @@ -32,7 +32,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-18 09:48:50.836233", + "modified": "2020-10-21 10:28:57.710549", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "SendCloud", diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py index 85c94388dc..d30af15eb5 100644 --- a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py +++ b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py @@ -16,9 +16,7 @@ class SendCloud(Document): def get_sendcloud_available_services(delivery_address, shipment_parcel): # Retrieve rates at SendCloud from specification stated. - enabled = frappe.db.get_single_value('SendCloud', 'enabled') - api_key = frappe.db.get_single_value('SendCloud', 'api_key') - api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') + api_key, api_secret, enabled = frappe.db.get_value('SendCloud', 'SendCloud', ['enabled', 'api_key', 'api_secret']) if not enabled or not api_key or not api_secret: return [] @@ -40,6 +38,7 @@ def get_sendcloud_available_services(delivery_address, shipment_parcel): available_services.append(available_service) return available_services except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint(_('Error occurred on SendCloud: {0}').format( str(exc)), indicator='orange', alert=True) @@ -53,9 +52,7 @@ def create_sendcloud_shipment( value_of_goods ): # Create a transaction at SendCloud - enabled = frappe.db.get_single_value('SendCloud', 'enabled') - api_key = frappe.db.get_single_value('SendCloud', 'api_key') - api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') + api_key, api_secret, enabled = frappe.db.get_value('SendCloud', 'SendCloud', ['enabled', 'api_key', 'api_secret']) if not enabled or not api_key or not api_secret: return [] @@ -105,6 +102,7 @@ def create_sendcloud_shipment( 'awb_number': awb_number } except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint(_('Error occurred while creating Shipment: {0}').format( str(exc)), indicator='orange', alert=True) @@ -130,17 +128,15 @@ def get_sendcloud_tracking_data(shipment_id): api_key = frappe.db.get_single_value('SendCloud', 'api_key') api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') shipment_id_list = shipment_id.split(', ') - tracking_url = '' awb_number = [] tracking_status = [] tracking_status_info = [] + tracking_urls = [] for ship_id in shipment_id_list: tracking_data_response = \ requests.get('https://panel.sendcloud.sc/api/v2/parcels/{id}'.format(id=ship_id), auth=(api_key, api_secret)) tracking_data = json.loads(tracking_data_response.text) - tracking_url_template = \ - '{{ _("Click here to Track Shipment") }}
' - tracking_url += frappe.render_template(tracking_url_template, {'tracking_url': tracking_data['parcel']['tracking_url']}) + tracking_urls.append(tracking_data['parcel']['tracking_url']) awb_number.append(tracking_data['parcel']['tracking_number']) tracking_status.append(tracking_data['parcel']['status']['message']) tracking_status_info.append(tracking_data['parcel']['status']['message']) @@ -148,9 +144,10 @@ def get_sendcloud_tracking_data(shipment_id): 'awb_number': ', '.join(awb_number), 'tracking_status': ', '.join(tracking_status), 'tracking_status_info': ', '.join(tracking_status_info), - 'tracking_url': tracking_url + 'tracking_url': ', '.join(tracking_urls) } except Exception as exc: + frappe.log_error(frappe.get_traceback()) frappe.msgprint(_('Error occurred while updating Shipment: {0}').format( str(exc)), indicator='orange', alert=True) diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index e7ef4c8ebd..362f6cf88e 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -68,6 +68,4 @@ def get_tracking_url(carrier, tracking_number): url_reference = frappe.get_value('Parcel Service', carrier, 'url_reference') if url_reference: tracking_url = frappe.render_template(url_reference, {'tracking_number': tracking_number}) - tracking_url_template = '{{ _("Click here to Track Shipment") }}' - tracking_url = frappe.render_template(tracking_url_template, {'tracking_url': tracking_url}) return tracking_url diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 00a66fa48e..26e4f1633e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -575,22 +575,24 @@ def make_shipment(source_name, target_doc=None): user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) target.pickup_contact_email = user.email pickup_contact_display = '{}'.format(user.full_name) - if user.email: - pickup_contact_display += '
' + user.email - if user.phone: - pickup_contact_display += '
' + user.phone - if user.mobile_no and not user.phone: - pickup_contact_display += '
' + user.mobile_no + if user: + if user.email: + pickup_contact_display += '
' + user.email + if user.phone: + pickup_contact_display += '
' + user.phone + if user.mobile_no and not user.phone: + pickup_contact_display += '
' + user.mobile_no target.pickup_contact = pickup_contact_display contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) delivery_contact_display = '{}'.format(source.contact_display) - if contact.email_id: - delivery_contact_display += '
' + contact.email_id - if contact.phone: - delivery_contact_display += '
' + contact.phone - if contact.mobile_no and not contact.phone: - delivery_contact_display += '
' + contact.mobile_no + if contact: + if contact.email_id: + delivery_contact_display += '
' + contact.email_id + if contact.phone: + delivery_contact_display += '
' + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += '
' + contact.mobile_no target.delivery_contact = delivery_contact_display doclist = get_mapped_doc("Delivery Note", source_name, { @@ -612,7 +614,7 @@ def make_shipment(source_name, target_doc=None): } }, "Delivery Note Item": { - "doctype": "Shipment Delivery Notes", + "doctype": "Shipment Delivery Note", "field_map": { "name": "prevdoc_detail_docname", "parent": "prevdoc_docname", diff --git a/erpnext/stock/doctype/shipment/api/utils.py b/erpnext/stock/doctype/shipment/api/utils.py deleted file mode 100644 index 1153933e81..0000000000 --- a/erpnext/stock/doctype/shipment/api/utils.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -import re - -def get_address(address_name): - address = frappe.db.get_value('Address', address_name, [ - 'address_title', - 'address_line1', - 'address_line2', - 'city', - 'pincode', - 'country', - ], as_dict=1) - address.country_code = frappe.db.get_value('Country', address.country, 'code').upper() - if not address.pincode or address.pincode == '': - frappe.throw(_("Postal Code is mandatory to continue.
\ - Please set Postal Code for Address {1}" - ).format(address_name, address_name)) - address.pincode = address.pincode.replace(' ', '') - address.city = address.city.strip() - return address - -def get_contact(contact_name): - contact = frappe.db.get_value('Contact', contact_name, [ - 'first_name', - 'last_name', - 'email_id', - 'phone', - 'mobile_no', - 'gender', - ], as_dict=1) - if not contact.last_name: - frappe.throw(_("Last Name is mandatory to continue.
\ - Please set Last Name for Contact {1}" - ).format(contact_name, contact_name)) - if not contact.phone: - contact.phone = contact.mobile_no - contact.phone_prefix = contact.phone[:3] - contact.phone = re.sub('[^A-Za-z0-9]+', '', contact.phone[3:]) - contact.email = contact.email_id - contact.title = 'MS' - if contact.gender == 'Male': - contact.title = 'MR' - return contact - -def get_company_contact(): - contact = frappe.db.get_value('User', frappe.session.user, [ - 'first_name', - 'last_name', - 'email', - 'phone', - 'mobile_no', - 'gender', - ], as_dict=1) - if not contact.phone: - contact.phone = contact.mobile_no - contact.phone_prefix = contact.phone[:3] - contact.phone = re.sub('[^A-Za-z0-9]+', '', contact.phone[3:]) - contact.title = 'MS' - if contact.gender == 'Male': - contact.title = 'MR' - return contact diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index e9f4484ab1..fc0b05f8af 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -2,11 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Shipment', { - setup: function(frm) { - if (frm.doc.__islocal) { - frm.trigger('pickup_type'); - } - }, address_query: function(frm, link_doctype, link_name, is_your_company_address) { return { query: 'frappe.contacts.doctype.address.address.address_query', @@ -99,7 +94,7 @@ frappe.ui.form.on('Shipment', { } return frm.events.contact_query(frm, link_doctype, link_name); }); - frm.set_query("delivery_note", "shipment_delivery_notes", function() { + frm.set_query("delivery_note", "shipment_delivery_note", function() { let customer = ''; if (frm.doc.delivery_to_type == "Customer") { customer = frm.doc.delivery_customer; @@ -127,27 +122,22 @@ frappe.ui.form.on('Shipment', { if (frm.doc.shipment_id) { frm.add_custom_button(__('Print Shipping Label'), function() { return frm.events.print_shipping_label(frm); - }); + }, __('Tools')); if (frm.doc.tracking_status != 'Delivered') { frm.add_custom_button(__('Update Tracking'), function() { return frm.events.update_tracking(frm, frm.doc.service_provider, frm.doc.shipment_id); - }); + }, __('Tools')); + + frm.add_custom_button(__('Track Status'), function() { + const urls = frm.doc.tracking_url.split(', '); + urls.forEach(url => window.open(url)); + }, __('View')); } } $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); - - if (frm.doc.delivery_from_type != 'Company') { - frm.set_df_property("delivery_contact_name", "reqd", 1); - } - if (frm.doc.pickup_from_type != 'Company') { - frm.set_df_property("pickup_contact_name", "reqd", 1); - } - else { - frm.toggle_display("pickup_contact_name", false); - } }, before_save: function(frm) { if (frm.doc.delivery_to_type == 'Company') { @@ -188,14 +178,10 @@ frappe.ui.form.on('Shipment', { pickup_from_type: function(frm) { if (frm.doc.pickup_from_type == 'Company') { frm.set_value("pickup_company", frappe.defaults.get_default('company')); - frm.set_df_property("pickup_contact_name", "reqd", 0); frm.set_value("pickup_customer", ''); frm.set_value("pickup_supplier", ''); - frm.toggle_display("pickup_contact_name", false); } else { - frm.set_df_property("pickup_contact_name", "reqd", 1); - frm.toggle_display("pickup_contact_name", true); frm.trigger('clear_pickup_fields'); } if (frm.doc.pickup_from_type == 'Customer') { @@ -206,20 +192,16 @@ frappe.ui.form.on('Shipment', { frm.set_value("pickup_customer", ''); frm.set_value("pickup_company", ''); } - frm.events.remove_notific_child_table(frm, 'shipment_notification_subscriptions', 'Pickup'); - frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscriptions', 'Pickup'); + frm.events.remove_notific_child_table(frm, 'shipment_notification_subscription', 'Pickup'); + frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscription', 'Pickup'); }, delivery_to_type: function(frm) { if (frm.doc.delivery_to_type == 'Company') { frm.set_value("delivery_company", frappe.defaults.get_default('company')); - frm.set_df_property("delivery_contact_name", "reqd", 0); frm.set_value("delivery_customer", ''); frm.set_value("delivery_supplier", ''); - frm.toggle_display("delivery_contact_name", false); } else { - frm.set_df_property("delivery_contact_name", "reqd", 1); - frm.toggle_display("delivery_contact_name", true); frm.trigger('clear_delivery_fields'); } if (frm.doc.delivery_to_type == 'Customer') { @@ -229,13 +211,13 @@ frappe.ui.form.on('Shipment', { if (frm.doc.delivery_to_type == 'Supplier') { frm.set_value("delivery_customer", ''); frm.set_value("delivery_company", ''); - frm.toggle_display("shipment_delivery_notes", false); + frm.toggle_display("shipment_delivery_note", false); } else { - frm.toggle_display("shipment_delivery_notes", true); + frm.toggle_display("shipment_delivery_note", true); } - frm.events.remove_notific_child_table(frm, 'shipment_notification_subscriptions', 'Delivery'); - frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscriptions', 'Delivery'); + frm.events.remove_notific_child_table(frm, 'shipment_notification_subscription', 'Delivery'); + frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscription', 'Delivery'); }, delivery_address_name: function(frm) { if (frm.doc.delivery_to_type == 'Company') { @@ -307,6 +289,51 @@ frappe.ui.form.on('Shipment', { frm.events.get_contact_display(frm, frm.doc.pickup_contact_name, 'Pickup'); } }, + pickup_contact_person: function(frm) { + if (frm.doc.pickup_contact_person) { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", + args: { user: frm.doc.pickup_contact_person }, + callback: function({ message }) { + const r = message; + let contact_display = `${r.first_name} ${r.last_name}`; + if (r.email) { + contact_display += `
${ r.email }`; + frm.set_value('pickup_contact_email', r.email); + } + if (r.phone) { + contact_display += `
${ r.phone }`; + } + if (r.mobile_no && !r.phone) { + contact_display += `
${ r.mobile_no }`; + } + frm.set_value('pickup_contact', contact_display); + } + }); + } else { + if (frm.doc.pickup_from_type === 'Company') { + frappe.call({ + method: "erpnext.stock.doctype.shipment.shipment.get_company_contact", + args: { user: frappe.session.user }, + callback: function({ message }) { + const r = message; + let contact_display = `${r.first_name} ${r.last_name}`; + if (r.email) { + contact_display += `
${ r.email }`; + frm.set_value('pickup_contact_email', r.email); + } + if (r.phone) { + contact_display += `
${ r.phone }`; + } + if (r.mobile_no && !r.phone) { + contact_display += `
${ r.mobile_no }`; + } + frm.set_value('pickup_contact', contact_display); + } + }); + } + } + }, set_company_contact: function(frm, delivery_type) { frappe.db.get_value('User', { name: frappe.session.user }, ['full_name', 'last_name', 'email', 'phone', 'mobile_no'], (r) => { if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { @@ -344,6 +371,7 @@ frappe.ui.form.on('Shipment', { } } }); + frm.set_value('pickup_contact_person', frappe.session.user); }, pickup_company: function(frm) { if (frm.doc.pickup_from_type == 'Company' && frm.doc.pickup_company) { @@ -372,14 +400,12 @@ frappe.ui.form.on('Shipment', { } }, pickup_customer: function(frm) { - frm.trigger('clear_pickup_fields'); if (frm.doc.pickup_customer) { frm.events.set_address_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup'); frm.events.set_contact_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup'); } }, pickup_supplier: function(frm) { - frm.trigger('clear_pickup_fields'); if (frm.doc.pickup_supplier) { frm.events.set_address_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup'); frm.events.set_contact_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup'); @@ -438,7 +464,7 @@ frappe.ui.form.on('Shipment', { }, pickup_date: function(frm) { if (frm.doc.pickup_date < frappe.datetime.get_today()) { - frappe.throw(__("Pickup Date cannot be in the past")); + frappe.throw(__("Pickup Date cannot be before this day")); } if (frm.doc.pickup_date == frappe.datetime.get_today()) { var pickup_time = frm.events.get_pickup_time(frm); @@ -487,65 +513,63 @@ frappe.ui.form.on('Shipment', { frm.set_value("pickup_to", pickup_to); }, clear_pickup_fields: function(frm) { - frm.set_value("pickup_address_name", ''); - frm.set_value("pickup_contact_name", ''); - frm.set_value("pickup_address", ''); - frm.set_value("pickup_contact", ''); - frm.set_value("pickup_contact_email", ''); + let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"]; + for (let field of fields){ + frm.set_value(field, ''); + } }, clear_delivery_fields: function(frm) { - frm.set_value("delivery_address_name", ''); - frm.set_value("delivery_contact_name", ''); - frm.set_value("delivery_address", ''); - frm.set_value("delivery_contact", ''); - frm.set_value("delivery_contact_email", ''); + let fields = ["delivery_address_name", "delivery_contact_name", "delivery_address", "delivery_contact", "delivery_contact_email"]; + for (let field of fields){ + frm.set_value(field, ''); + } }, pickup_from_send_shipping_notification: function(frm, cdt, cdn) { if (frm.doc.pickup_contact_email && frm.doc.pickup_from_send_shipping_notification - && !validate_duplicate(frm, 'shipment_notification_subscriptions', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { - let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscriptions", "shipment_notification_subscriptions"); + && !validate_duplicate(frm, 'shipment_notification_subscription', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { + let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscription", "shipment_notification_subscription"); row.email = frm.doc.pickup_contact_email; - frm.refresh_fields("shipment_notification_subscriptions"); + frm.refresh_fields("shipment_notification_subscription"); } if (!frm.doc.pickup_from_send_shipping_notification) { - frm.events.remove_email_row(frm, 'shipment_notification_subscriptions', frm.doc.pickup_contact_email); - frm.refresh_fields("shipment_notification_subscriptions"); + frm.events.remove_email_row(frm, 'shipment_notification_subscription', frm.doc.pickup_contact_email); + frm.refresh_fields("shipment_notification_subscription"); } }, pickup_from_subscribe_to_status_updates: function(frm, cdt, cdn) { if (frm.doc.pickup_contact_email && frm.doc.pickup_from_subscribe_to_status_updates - && !validate_duplicate(frm, 'shipment_status_update_subscriptions', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { - let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscriptions", "shipment_status_update_subscriptions"); + && !validate_duplicate(frm, 'shipment_status_update_subscription', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { + let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscription", "shipment_status_update_subscription"); row.email = frm.doc.pickup_contact_email; - frm.refresh_fields("shipment_status_update_subscriptions"); + frm.refresh_fields("shipment_status_update_subscription"); } if (!frm.doc.pickup_from_subscribe_to_status_updates) { - frm.events.remove_email_row(frm, 'shipment_status_update_subscriptions', frm.doc.pickup_contact_email); - frm.refresh_fields("shipment_status_update_subscriptions"); + frm.events.remove_email_row(frm, 'shipment_status_update_subscription', frm.doc.pickup_contact_email); + frm.refresh_fields("shipment_status_update_subscription"); } }, delivery_to_send_shipping_notification: function(frm, cdt, cdn) { if (frm.doc.delivery_contact_email && frm.doc.delivery_to_send_shipping_notification - && !validate_duplicate(frm, 'shipment_notification_subscriptions', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)){ - let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscriptions", "shipment_notification_subscriptions"); + && !validate_duplicate(frm, 'shipment_notification_subscription', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)){ + let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscription", "shipment_notification_subscription"); row.email = frm.doc.delivery_contact_email; - frm.refresh_fields("shipment_notification_subscriptions"); + frm.refresh_fields("shipment_notification_subscription"); } if (!frm.doc.delivery_to_send_shipping_notification) { - frm.events.remove_email_row(frm, 'shipment_notification_subscriptions', frm.doc.delivery_contact_email); - frm.refresh_fields("shipment_notification_subscriptions"); + frm.events.remove_email_row(frm, 'shipment_notification_subscription', frm.doc.delivery_contact_email); + frm.refresh_fields("shipment_notification_subscription"); } }, delivery_to_subscribe_to_status_updates: function(frm, cdt, cdn) { if (frm.doc.delivery_contact_email && frm.doc.delivery_to_subscribe_to_status_updates - && !validate_duplicate(frm, 'shipment_status_update_subscriptions', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)) { - let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscriptions", "shipment_status_update_subscriptions"); + && !validate_duplicate(frm, 'shipment_status_update_subscription', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)) { + let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscription", "shipment_status_update_subscription"); row.email = frm.doc.delivery_contact_email; - frm.refresh_fields("shipment_status_update_subscriptions"); + frm.refresh_fields("shipment_status_update_subscription"); } if (!frm.doc.delivery_to_subscribe_to_status_updates) { - frm.events.remove_email_row(frm, 'shipment_status_update_subscriptions', frm.doc.delivery_contact_email); - frm.refresh_fields("shipment_status_update_subscriptions"); + frm.events.remove_email_row(frm, 'shipment_status_update_subscription', frm.doc.delivery_contact_email); + frm.refresh_fields("shipment_status_update_subscription"); } }, remove_email_row: function(frm, table, fieldname) { @@ -589,7 +613,7 @@ frappe.ui.form.on('Shipment', { shipment_parcel: frm.doc.shipment_parcel, description_of_content: frm.doc.description_of_content, pickup_date: frm.doc.pickup_date, - pickup_contact_name: frm.doc.pickup_contact_name, + pickup_contact_name: frm.doc.pickup_from_type === 'Company' ? frm.doc.pickup_contact_person : frm.doc.pickup_contact_name, delivery_contact_name: frm.doc.delivery_contact_name, value_of_goods: frm.doc.value_of_goods }, @@ -639,7 +663,7 @@ frappe.ui.form.on('Shipment', { }, update_tracking: function(frm, service_provider, shipment_id) { let delivery_notes = []; - (frm.doc.shipment_delivery_notes || []).forEach((d) => { + (frm.doc.shipment_delivery_note || []).forEach((d) => { delivery_notes.push(d.delivery_note); }); frappe.call({ @@ -661,14 +685,13 @@ frappe.ui.form.on('Shipment', { } }); -frappe.ui.form.on('Shipment Delivery Notes', { +frappe.ui.form.on('Shipment Delivery Note', { delivery_note: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.delivery_note) { let row_index = row.idx - 1; - if(validate_duplicate(frm, 'shipment_delivery_notes', row.delivery_note, row_index)) { - cur_frm.get_field('shipment_delivery_notes').grid.grid_rows[row_index].remove(); - frappe.throw(__(`You have entered duplicate Delivery Notes. Please rectify and try again.`)); + if(validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) { + frappe.throw(__(`You have entered a duplicate Delivery Note on Row ${row.idx}. Please rectify and try again.`)); } } }, @@ -683,29 +706,27 @@ frappe.ui.form.on('Shipment Delivery Notes', { }); var validate_duplicate = function(frm, table, fieldname, index){ - let duplicate = false; - $.each(frm.doc[table], function(i, detail) { - // Email duplicate validation - if(detail.email === fieldname && !(index === i)) { - duplicate = true; - return; - } - - // Delivery Note duplicate validation - if(detail.delivery_note === fieldname && !(index === i)) { - duplicate = true; - return; - } - }); - return duplicate; + return ( + table === 'shipment_delivery_note' + ? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i)) + : frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i)) + ); }; function select_from_available_services(frm, available_services) { var headers = [ __("Service Provider"), __("Carrier"), __("Carrier’s Service"), __("Price"), "" ]; cur_frm.render_available_services = function(d, headers, data){ + const arranged_data = data.reduce((prev, curr) => { + if (curr.is_preferred) { + prev.preferred_services.push(curr); + } else { + prev.other_services.push(curr); + } + return prev; + }, { preferred_services: [], other_services: [] }); d.fields_dict.available_services.$wrapper.html( frappe.render_template('shipment_service_selector', - {'header_columns': headers, 'data': data} + {'header_columns': headers, 'data': arranged_data} ) ); }; @@ -722,18 +743,18 @@ function select_from_available_services(frm, available_services) { cur_frm.render_available_services(d, headers, available_services); let shipment_notific_email = []; let tracking_notific_email = []; - (frm.doc.shipment_notification_subscriptions || []).forEach((d) => { + (frm.doc.shipment_notification_subscription || []).forEach((d) => { if (!d.unsubscribed) { shipment_notific_email.push(d.email); } }); - (frm.doc.shipment_status_update_subscriptions || []).forEach((d) => { + (frm.doc.shipment_status_update_subscription || []).forEach((d) => { if (!d.unsubscribed) { tracking_notific_email.push(d.email); } }); let delivery_notes = []; - (frm.doc.shipment_delivery_notes || []).forEach((d) => { + (frm.doc.shipment_delivery_note || []).forEach((d) => { delivery_notes.push(d.delivery_note); }); cur_frm.select_row = function(service_data){ @@ -750,7 +771,7 @@ function select_from_available_services(frm, available_services) { shipment_parcel: frm.doc.shipment_parcel, description_of_content: frm.doc.description_of_content, pickup_date: frm.doc.pickup_date, - pickup_contact_name: frm.doc.pickup_contact_name, + pickup_contact_name: frm.doc.pickup_from_type === 'Company' ? frm.doc.pickup_contact_person : frm.doc.pickup_contact_name, delivery_contact_name: frm.doc.delivery_contact_name, value_of_goods: frm.doc.value_of_goods, service_data: service_data, diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index b6656a2b72..bbfbb719be 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -14,6 +14,7 @@ "pickup", "pickup_address_name", "pickup_address", + "pickup_contact_person", "pickup_contact_name", "pickup_contact_email", "pickup_contact", @@ -32,17 +33,17 @@ "notification_details_section", "pickup_from_send_shipping_notification", "pickup_from_subscribe_to_status_updates", - "shipment_notification_subscriptions", + "shipment_notification_subscription", "column_break_27", "delivery_to_send_shipping_notification", "delivery_to_subscribe_to_status_updates", - "shipment_status_update_subscriptions", + "shipment_status_update_subscription", "parcels_section", "shipment_parcel", "parcel_template", "add_template", "column_break_28", - "shipment_delivery_notes", + "shipment_delivery_note", "shipment_details_section", "pallets", "value_of_goods", @@ -125,10 +126,11 @@ "read_only": 1 }, { - "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type == \"Company\"", + "depends_on": "eval: doc.pickup_customer || doc.pickup_supplier || doc.pickup_from_type !== \"Company\"", "fieldname": "pickup_contact_name", "fieldtype": "Link", "label": "Contact", + "mandatory_depends_on": "eval: doc.pickup_from_type !== 'Company'", "options": "Contact" }, { @@ -206,6 +208,7 @@ "fieldname": "delivery_contact_name", "fieldtype": "Link", "label": "Contact", + "mandatory_depends_on": "eval: doc.delivery_from_type !== 'Company'", "options": "Contact" }, { @@ -239,12 +242,6 @@ "fieldtype": "Check", "label": "Subscribe to status updates" }, - { - "fieldname": "shipment_notification_subscriptions", - "fieldtype": "Table", - "label": "Shipment Notification Subscriptions", - "options": "Shipment Notification Subscriptions" - }, { "fieldname": "column_break_27", "fieldtype": "Column Break" @@ -261,12 +258,6 @@ "fieldtype": "Check", "label": "Subscribe to status updates" }, - { - "fieldname": "shipment_status_update_subscriptions", - "fieldtype": "Table", - "label": "Shipment Status Update Subscriptions", - "options": "Shipment Status Update Subscriptions" - }, { "fieldname": "parcels_section", "fieldtype": "Section Break", @@ -293,12 +284,6 @@ "fieldname": "column_break_28", "fieldtype": "Column Break" }, - { - "fieldname": "shipment_delivery_notes", - "fieldtype": "Table", - "label": "Shipment Delivery Notes", - "options": "Shipment Delivery Notes" - }, { "fieldname": "shipment_details_section", "fieldtype": "Section Break", @@ -328,16 +313,14 @@ { "default": "09:00", "fieldname": "pickup_from", - "fieldtype": "Select", - "label": "Pickup from", - "options": "09:00\n09:30\n10:00\n10:30\n11:00\n11:30\n12:00\n12:30\n13:00\n13:30\n14:00\n14:30\n15:00\n15:30\n16:00\n16:30\n17:00\n17:30\n18:00\n18:30\n19:00" + "fieldtype": "Time", + "label": "Pickup from" }, { "default": "17:00", "fieldname": "pickup_to", - "fieldtype": "Select", - "label": "Pickup to", - "options": "09:00\n09:30\n10:00\n10:30\n11:00\n11:30\n12:00\n12:30\n13:00\n13:30\n14:00\n14:30\n15:00\n15:30\n16:00\n16:30\n17:00\n17:30\n18:00\n18:30\n19:00" + "fieldtype": "Time", + "label": "Pickup to" }, { "fieldname": "column_break_36", @@ -374,13 +357,15 @@ }, { "fieldname": "service_provider", - "fieldtype": "Read Only", - "label": "Service Provider" + "fieldtype": "Data", + "label": "Service Provider", + "read_only": 1 }, { "fieldname": "shipment_id", - "fieldtype": "Read Only", - "label": "Shipment ID" + "fieldtype": "Data", + "label": "Shipment ID", + "read_only": 1 }, { "fieldname": "shipment_amount", @@ -399,23 +384,27 @@ { "fieldname": "tracking_url", "fieldtype": "Small Text", + "hidden": 1, "label": "Tracking URL", "read_only": 1 }, { "fieldname": "carrier", - "fieldtype": "Read Only", - "label": "Carrier" + "fieldtype": "Data", + "label": "Carrier", + "read_only": 1 }, { "fieldname": "carrier_service", - "fieldtype": "Read Only", - "label": "Carrier Service" + "fieldtype": "Data", + "label": "Carrier Service", + "read_only": 1 }, { "fieldname": "awb_number", - "fieldtype": "Read Only", - "label": "AWB Number" + "fieldtype": "Data", + "label": "AWB Number", + "read_only": 1 }, { "fieldname": "tracking_status", @@ -449,11 +438,37 @@ "fieldtype": "Select", "label": "Incoterm", "options": "EXW (Ex Works)\nFCA (Free Carrier)\nCPT (Carriage Paid To)\nCIP (Carriage and Insurance Paid to)\nDPU (Delivered At Place Unloaded)\nDAP (Delivered At Place)\nDDP (Delivered Duty Paid)" + }, + { + "fieldname": "shipment_delivery_note", + "fieldtype": "Table", + "label": "Shipment Delivery Note", + "options": "Shipment Delivery Note" + }, + { + "fieldname": "shipment_notification_subscription", + "fieldtype": "Table", + "label": "Shipment Notification Subscription", + "options": "Shipment Notification Subscription" + }, + { + "fieldname": "shipment_status_update_subscription", + "fieldtype": "Table", + "label": "Shipment Status Update Subscription", + "options": "Shipment Status Update Subscription" + }, + { + "depends_on": "eval:doc.pickup_from_type === 'Company'", + "fieldname": "pickup_contact_person", + "fieldtype": "Link", + "label": "Pickup Contact Person", + "mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'", + "options": "User" } ], "is_submittable": 1, "links": [], - "modified": "2020-07-24 11:44:30.904612", + "modified": "2020-09-29 13:59:50.241744", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index e059bacfa1..9b3c976ca4 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe import json from frappe import _ +from frappe.utils import flt from frappe.model.document import Document from erpnext.accounts.party import get_party_shipping_address from frappe.contacts.doctype.contact.contact import get_default_contact @@ -32,7 +33,7 @@ class Shipment(Document): def validate_weight(self): for parcel in self.shipment_parcel: - if parcel.weight <= 0: + if flt(parcel.weight) <= 0: frappe.throw(_('Parcel weight cannot be 0')) @frappe.whitelist() @@ -52,12 +53,12 @@ def fetch_shipping_rates(pickup_from_type, delivery_to_type, pickup_address_name if pickup_from_type != 'Company': pickup_contact = get_contact(pickup_contact_name) else: - pickup_contact = get_company_contact() + pickup_contact = get_company_contact(user=pickup_contact_name) if delivery_to_type != 'Company': delivery_contact = get_contact(delivery_contact_name) else: - delivery_contact = get_company_contact() + delivery_contact = get_company_contact(user=pickup_contact_name) letmeship_prices = get_letmeship_available_services( delivery_to_type=delivery_to_type, pickup_address=pickup_address, @@ -104,12 +105,12 @@ def create_shipment(shipment, pickup_from_type, delivery_to_type, pickup_address if pickup_from_type != 'Company': pickup_contact = get_contact(pickup_contact_name) else: - pickup_contact = get_company_contact() + pickup_contact = get_company_contact(user=pickup_contact_name) if delivery_to_type != 'Company': delivery_contact = get_contact(delivery_contact_name) else: - delivery_contact = get_company_contact() + delivery_contact = get_company_contact(user=pickup_contact_name) if service_info['service_provider'] == LETMESHIP_PROVIDER: shipment_info = create_letmeship_shipment( pickup_address=pickup_address, @@ -150,12 +151,9 @@ def create_shipment(shipment, pickup_from_type, delivery_to_type, pickup_address ) if shipment_info: - frappe.db.set_value('Shipment', shipment, 'service_provider', shipment_info.get('service_provider')) - frappe.db.set_value('Shipment', shipment, 'carrier', shipment_info.get('carrier')) - frappe.db.set_value('Shipment', shipment, 'carrier_service', shipment_info.get('carrier_service')) - frappe.db.set_value('Shipment', shipment, 'shipment_id', shipment_info.get('shipment_id')) - frappe.db.set_value('Shipment', shipment, 'shipment_amount', shipment_info.get('shipment_amount')) - frappe.db.set_value('Shipment', shipment, 'awb_number', shipment_info.get('awb_number')) + fields = ['service_provider', 'carrier', 'carrier_service', 'shipment_id', 'shipment_amount', 'awb_number'] + for field in fields: + frappe.db.set_value('Shipment', shipment, field, shipment_info.get(field)) frappe.db.set_value('Shipment', shipment, 'status', 'Booked') if delivery_notes: update_delivery_note(delivery_notes=delivery_notes, shipment_info=shipment_info) @@ -277,9 +275,17 @@ def get_contact(contact_name): contact.phone = contact.mobile_no return contact +def match_parcel_service_type_carrier(shipment_prices, reference): + for idx, prices in enumerate(shipment_prices): + service_name = match_parcel_service_type_alias(prices.get(reference[0]), prices.get(reference[1])) + is_preferred = frappe.db.get_value('Parcel Service Type', service_name, 'show_in_preferred_services_list') + shipment_prices[idx].service_name = service_name + shipment_prices[idx].is_preferred = is_preferred + return shipment_prices -def get_company_contact(): - contact = frappe.db.get_value('User', frappe.session.user, [ +@frappe.whitelist() +def get_company_contact(user): + contact = frappe.db.get_value('User', user, [ 'first_name', 'last_name', 'email', @@ -289,12 +295,4 @@ def get_company_contact(): ], as_dict=1) if not contact.phone: contact.phone = contact.mobile_no - return contact - -def match_parcel_service_type_carrier(shipment_prices, reference): - for idx, prices in enumerate(shipment_prices): - service_name = match_parcel_service_type_alias(prices.get(reference[0]), prices.get(reference[1])) - is_preferred = frappe.db.get_value('Parcel Service Type', service_name, 'show_in_preferred_services_list') - shipment_prices[idx].service_name = service_name - shipment_prices[idx].is_preferred = is_preferred - return shipment_prices + return contact \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment/shipment_service_selector.html b/erpnext/stock/doctype/shipment/shipment_service_selector.html index ed9b8bf400..4ccbe34e9e 100644 --- a/erpnext/stock/doctype/shipment/shipment_service_selector.html +++ b/erpnext/stock/doctype/shipment/shipment_service_selector.html @@ -1,59 +1,63 @@ -{% if (data.length) { %} -
+{% if (data.preferred_services.length || data.other_services.length) { %} +
{{ __("Preferred Services") }}
- - - - {% for (var i = 0; i < header_columns.length; i++) { %} - - {% } %} - - - - {% for (var i = 0; i < data.length; i++) { %} - {% if (data[i].is_preferred) { %} + {% if (data.preferred_services.length) { %} +
{{ header_columns[i] }}
+ + + {% for (var i = 0; i < header_columns.length; i++) { %} + + {% } %} + + + + {% for (var i = 0; i < data.preferred_services.length; i++) { %} - - - - + + + + {% } %} - {% } %} - -
{{ header_columns[i] }}
{{ data[i].service_provider }}{{ data[i].carrier }}{{ data[i].service_name }}{{ format_currency(data[i].total_price, 'EUR', 2) }}{{ data.preferred_services[i].service_provider }}{{ data.preferred_services[i].carrier }}{{ data.preferred_services[i].service_name }}{{ format_currency(data.preferred_services[i].total_price, 'EUR', 2) }} -
+ + + {% } else { %} +
{{ __("No Preferred Services Available") }}
+ {% } %}
{{ __("Other Services") }}
- - - - {% for (var i = 0; i < header_columns.length; i++) { %} - - {% } %} - - - - {% for (var i = 0; i < data.length; i++) { %} - {% if (!data[i].is_preferred) { %} + {% if (data.other_services.length) { %} +
{{ header_columns[i] }}
+ + + {% for (var i = 0; i < header_columns.length; i++) { %} + + {% } %} + + + + {% for (var i = 0; i < data.other_services.length; i++) { %} - - - - + + + + {% } %} - {% } %} - -
{{ header_columns[i] }}
{{ data[i].service_provider }}{{ data[i].carrier }}{{ data[i].service_name }}{{ format_currency(data[i].total_price, 'EUR', 2) }}{{ data.other_services[i].service_provider }}{{ data.other_services[i].carrier }}{{ data.other_services[i].service_name }}{{ format_currency(data.other_services[i].total_price, 'EUR', 2) }} -
+ + + {% } else { %} +
{{ __("No Services Available") }}
+ {% } %}
{% } else { %}
{{ __("No Services Available") }}
diff --git a/erpnext/stock/doctype/shipment_delivery_notes/__init__.py b/erpnext/stock/doctype/shipment_delivery_note/__init__.py similarity index 100% rename from erpnext/stock/doctype/shipment_delivery_notes/__init__.py rename to erpnext/stock/doctype/shipment_delivery_note/__init__.py diff --git a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json similarity index 95% rename from erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json rename to erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json index fbc01d9a24..9651e3f945 100644 --- a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.json +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json @@ -31,7 +31,7 @@ "modified": "2020-07-09 12:55:01.134270", "modified_by": "Administrator", "module": "Stock", - "name": "Shipment Delivery Notes", + "name": "Shipment Delivery Note", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py similarity index 86% rename from erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py rename to erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py index ed936c60f8..4342151605 100644 --- a/erpnext/stock/doctype/shipment_delivery_notes/shipment_delivery_notes.py +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class ShipmentDeliveryNotes(Document): +class ShipmentDeliveryNote(Document): pass diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py b/erpnext/stock/doctype/shipment_notification_subscription/__init__.py similarity index 100% rename from erpnext/stock/doctype/shipment_notification_subscriptions/__init__.py rename to erpnext/stock/doctype/shipment_notification_subscription/__init__.py diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json b/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json similarity index 93% rename from erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json rename to erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json index bd9b8003a8..d927d9902e 100644 --- a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.json +++ b/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json @@ -30,7 +30,7 @@ "modified": "2020-07-09 12:55:14.217387", "modified_by": "Administrator", "module": "Stock", - "name": "Shipment Notification Subscriptions", + "name": "Shipment Notification Subscription", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py b/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py similarity index 83% rename from erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py rename to erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py index 28ead7fab8..c816e4343c 100644 --- a/erpnext/stock/doctype/shipment_notification_subscriptions/shipment_notification_subscriptions.py +++ b/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class ShipmentNotificationSubscriptions(Document): +class ShipmentNotificationSubscription(Document): pass diff --git a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json index ec2bb1c9b3..4735d9f886 100644 --- a/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json +++ b/erpnext/stock/doctype/shipment_parcel_template/shipment_parcel_template.json @@ -1,26 +1,18 @@ { "actions": [], - "autoname": "field:preset_name", + "autoname": "field:parcel_template_name", "creation": "2020-07-09 11:43:43.470339", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "preset_name", + "parcel_template_name", "length", "width", "height", "weight" ], "fields": [ - { - "fieldname": "preset_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Preset Name", - "reqd": 1, - "unique": 1 - }, { "fieldname": "length", "fieldtype": "Int", @@ -49,10 +41,18 @@ "label": "Weight (kg)", "precision": "1", "reqd": 1 + }, + { + "fieldname": "parcel_template_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Parcel Template Name", + "reqd": 1, + "unique": 1 } ], "links": [], - "modified": "2020-07-10 12:53:22.772826", + "modified": "2020-09-28 12:51:00.320421", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Parcel Template", diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py b/erpnext/stock/doctype/shipment_status_update_subscription/__init__.py similarity index 100% rename from erpnext/stock/doctype/shipment_status_update_subscriptions/__init__.py rename to erpnext/stock/doctype/shipment_status_update_subscription/__init__.py diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json b/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json similarity index 93% rename from erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json rename to erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json index 3b86b400d8..a7fe4a4a0a 100644 --- a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.json +++ b/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json @@ -30,7 +30,7 @@ "modified": "2020-07-09 12:55:27.615463", "modified_by": "Administrator", "module": "Stock", - "name": "Shipment Status Update Subscriptions", + "name": "Shipment Status Update Subscription", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py b/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py similarity index 83% rename from erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py rename to erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py index a8e31ea778..1b006d7efc 100644 --- a/erpnext/stock/doctype/shipment_status_update_subscriptions/shipment_status_update_subscriptions.py +++ b/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class ShipmentStatusUpdateSubscriptions(Document): +class ShipmentStatusUpdateSubscription(Document): pass From ac3c1f14938655ed0c01315c8ca675d1fbb442aa Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Fri, 20 Nov 2020 08:12:29 +0000 Subject: [PATCH 217/229] chore: remove packlink, letmeship, and sendcloud files --- .../doctype/letmeship/__init__.py | 0 .../doctype/letmeship/letmeship.js | 8 - .../doctype/letmeship/letmeship.json | 55 --- .../doctype/letmeship/letmeship.py | 386 ------------------ .../doctype/letmeship/test_letmeship.py | 10 - .../doctype/packlink/__init__.py | 0 .../doctype/packlink/packlink.js | 8 - .../doctype/packlink/packlink.json | 48 --- .../doctype/packlink/packlink.py | 240 ----------- .../doctype/packlink/test_packlink.py | 10 - .../doctype/sendcloud/__init__.py | 0 .../doctype/sendcloud/sendcloud.js | 8 - .../doctype/sendcloud/sendcloud.json | 56 --- .../doctype/sendcloud/sendcloud.py | 168 -------- .../doctype/sendcloud/test_sendcloud.py | 10 - .../stock/doctype/parcel_service/__init__.py | 0 .../doctype/parcel_service/parcel_service.js | 8 - .../parcel_service/parcel_service.json | 56 --- .../doctype/parcel_service/parcel_service.py | 10 - .../parcel_service/test_parcel_service.py | 10 - .../doctype/parcel_service_type/__init__.py | 0 .../parcel_service_type.js | 12 - .../parcel_service_type.json | 89 ---- .../parcel_service_type.py | 22 - .../test_parcel_service_type.py | 10 - .../parcel_service_type_alias/__init__.py | 0 .../parcel_service_type_alias.json | 41 -- .../parcel_service_type_alias.py | 10 - erpnext/stock/doctype/shipment/shipment.js | 183 --------- erpnext/stock/doctype/shipment/shipment.json | 26 +- erpnext/stock/doctype/shipment/shipment.py | 243 +---------- .../shipment/shipment_service_selector.html | 74 ---- 32 files changed, 10 insertions(+), 1791 deletions(-) delete mode 100644 erpnext/erpnext_integrations/doctype/letmeship/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/letmeship/letmeship.js delete mode 100644 erpnext/erpnext_integrations/doctype/letmeship/letmeship.json delete mode 100644 erpnext/erpnext_integrations/doctype/letmeship/letmeship.py delete mode 100644 erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py delete mode 100644 erpnext/erpnext_integrations/doctype/packlink/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/packlink/packlink.js delete mode 100644 erpnext/erpnext_integrations/doctype/packlink/packlink.json delete mode 100644 erpnext/erpnext_integrations/doctype/packlink/packlink.py delete mode 100644 erpnext/erpnext_integrations/doctype/packlink/test_packlink.py delete mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/__init__.py delete mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js delete mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json delete mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py delete mode 100644 erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py delete mode 100644 erpnext/stock/doctype/parcel_service/__init__.py delete mode 100644 erpnext/stock/doctype/parcel_service/parcel_service.js delete mode 100644 erpnext/stock/doctype/parcel_service/parcel_service.json delete mode 100644 erpnext/stock/doctype/parcel_service/parcel_service.py delete mode 100644 erpnext/stock/doctype/parcel_service/test_parcel_service.py delete mode 100644 erpnext/stock/doctype/parcel_service_type/__init__.py delete mode 100644 erpnext/stock/doctype/parcel_service_type/parcel_service_type.js delete mode 100644 erpnext/stock/doctype/parcel_service_type/parcel_service_type.json delete mode 100644 erpnext/stock/doctype/parcel_service_type/parcel_service_type.py delete mode 100644 erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py delete mode 100644 erpnext/stock/doctype/parcel_service_type_alias/__init__.py delete mode 100644 erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json delete mode 100644 erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py delete mode 100644 erpnext/stock/doctype/shipment/shipment_service_selector.html diff --git a/erpnext/erpnext_integrations/doctype/letmeship/__init__.py b/erpnext/erpnext_integrations/doctype/letmeship/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js deleted file mode 100644 index 1e5e372dff..0000000000 --- a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('LetMeShip', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json deleted file mode 100644 index 94b001ed08..0000000000 --- a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "actions": [], - "creation": "2020-07-23 10:55:19.669830", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enabled", - "api_id", - "api_password" - ], - "fields": [ - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - }, - { - "fieldname": "api_id", - "fieldtype": "Data", - "label": "API ID", - "read_only_depends_on": "eval:doc.enabled == 0" - }, - { - "fieldname": "api_password", - "fieldtype": "Password", - "label": "API Password", - "read_only_depends_on": "eval:doc.enabled == 0" - } - ], - "issingle": 1, - "links": [], - "modified": "2020-10-21 10:28:37.607717", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "LetMeShip", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py deleted file mode 100644 index 162c1edb37..0000000000 --- a/erpnext/erpnext_integrations/doctype/letmeship/letmeship.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import requests -import frappe -import json -import re -from frappe import _ -from frappe.model.document import Document -from erpnext.erpnext_integrations.utils import get_tracking_url - -LETMESHIP_PROVIDER = 'LetMeShip' - -class LetMeShip(Document): - pass - -def get_letmeship_available_services(delivery_to_type, pickup_address, - delivery_address, shipment_parcel, description_of_content, pickup_date, - value_of_goods, pickup_contact=None, delivery_contact=None): - # Retrieve rates at LetMeShip from specification stated. - api_id, api_password, enabled = frappe.db.get_value('LetMeShip', 'LetMeShip', ['enabled', 'api_password', 'api_id']) - if not enabled or not api_id or not api_password: - return [] - - set_letmeship_specific_fields(pickup_contact, delivery_contact) - - # LetMeShip have limit of 30 characters for Company field - if len(pickup_address.address_title) > 30: - pickup_address.address_title = pickup_address.address_title[:30] - if len(delivery_address.address_title) > 30: - delivery_address.address_title = delivery_address.address_title[:30] - parcel_list = get_parcel_list(json.loads(shipment_parcel), description_of_content) - - url = 'https://api.letmeship.com/v1/available' - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Access-Control-Allow-Origin': 'string' - } - payload = generate_payload( - pickup_address=pickup_address, - pickup_contact=pickup_contact, - delivery_address=delivery_address, - delivery_contact=delivery_contact, - description_of_content=description_of_content, - value_of_goods=value_of_goods, - parcel_list=parcel_list, - pickup_date=pickup_date - ) - try: - available_services = [] - response_data = requests.post( - url=url, - auth=(api_id, api_password), - headers=headers, - data=json.dumps(payload) - ) - response_data = json.loads(response_data.text) - if 'serviceList' in response_data: - for response in response_data['serviceList']: - available_service = frappe._dict() - basic_info = response['baseServiceDetails'] - price_info = basic_info['priceInfo'] - available_service.service_provider = LETMESHIP_PROVIDER - available_service.id = basic_info['id'] - available_service.carrier = basic_info['carrier'] - available_service.carrier_name = basic_info['name'] - available_service.service_name = '' - available_service.is_preferred = 0 - available_service.real_weight = price_info['realWeight'] - available_service.total_price = price_info['netPrice'] - available_service.price_info = price_info - available_services.append(available_service) - return available_services - else: - frappe.throw( - _('Error occurred while fetching LetMeShip prices: {0}') - .format(response_data['message']) - ) - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint( - _('Error occurred while fetching LetMeShip Prices: {0}') - .format(str(exc)), - indicator='orange', - alert=True - ) - return [] - - -def create_letmeship_shipment(pickup_address, delivery_address, shipment_parcel, description_of_content, - pickup_date, value_of_goods, service_info, shipment_notific_email, tracking_notific_email, - pickup_contact=None, delivery_contact=None): - # Create a transaction at LetMeShip - # LetMeShip have limit of 30 characters for Company field - api_id, api_password, enabled = frappe.db.get_value('LetMeShip', 'LetMeShip', ['enabled', 'api_password', 'api_id']) - if not enabled or not api_id or not api_password: - return [] - - set_letmeship_specific_fields(pickup_contact, delivery_contact) - - if len(pickup_address.address_title) > 30: - pickup_address.address_title = pickup_address.address_title[:30] - if len(delivery_address.address_title) > 30: - delivery_address.address_title = delivery_address.address_title[:30] - - parcel_list = get_parcel_list(json.loads(shipment_parcel), description_of_content) - url = 'https://api.letmeship.com/v1/shipments' - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Access-Control-Allow-Origin': 'string' - } - payload = generate_payload( - pickup_address=pickup_address, - pickup_contact=pickup_contact, - delivery_address=delivery_address, - delivery_contact=delivery_contact, - description_of_content=description_of_content, - value_of_goods=value_of_goods, - parcel_list=parcel_list, - pickup_date=pickup_date, - service_info=service_info, - tracking_notific_email=tracking_notific_email, - shipment_notific_email=shipment_notific_email - ) - try: - response_data = requests.post( - url=url, - auth=(api_id, api_password), - headers=headers, - data=json.dumps(payload) - ) - response_data = json.loads(response_data.text) - if 'shipmentId' in response_data: - shipment_amount = response_data['service']['priceInfo']['totalPrice'] - awb_number = '' - url = 'https://api.letmeship.com/v1/shipments/{id}'.format(id=response_data['shipmentId']) - tracking_response = requests.get(url, auth=(api_id, api_password),headers=headers) - tracking_response_data = json.loads(tracking_response.text) - if 'trackingData' in tracking_response_data: - for parcel in tracking_response_data['trackingData']['parcelList']: - if 'awbNumber' in parcel: - awb_number = parcel['awbNumber'] - return { - 'service_provider': LETMESHIP_PROVIDER, - 'shipment_id': response_data['shipmentId'], - 'carrier': service_info['carrier'], - 'carrier_service': service_info['service_name'], - 'shipment_amount': shipment_amount, - 'awb_number': awb_number, - } - elif 'message' in response_data: - frappe.throw( - _('Error occurred while creating Shipment: {0}') - .format(response_data['message']) - ) - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint( - _('Error occurred while creating Shipment: {0}') - .format(str(exc)), - indicator='orange', - alert=True - ) - -def generate_payload( - pickup_address, - pickup_contact, - delivery_address, - delivery_contact, - description_of_content, - value_of_goods, - parcel_list, - pickup_date, - service_info=None, - tracking_notific_email=None, - shipment_notific_email=None -): - payload = { - 'pickupInfo': { - 'address': { - 'countryCode': pickup_address.country_code, - 'zip': pickup_address.pincode, - 'city': pickup_address.city, - 'street': pickup_address.address_line1, - 'addressInfo1': pickup_address.address_line2, - 'houseNo': '', - }, - 'company': pickup_address.address_title, - 'person': { - 'title': pickup_contact.title, - 'firstname': pickup_contact.first_name, - 'lastname': pickup_contact.last_name - }, - 'phone': { - 'phoneNumber': pickup_contact.phone, - 'phoneNumberPrefix': pickup_contact.phone_prefix - }, - 'email': pickup_contact.email, - }, - 'deliveryInfo': { - 'address': { - 'countryCode': delivery_address.country_code, - 'zip': delivery_address.pincode, - 'city': delivery_address.city, - 'street': delivery_address.address_line1, - 'addressInfo1': delivery_address.address_line2, - 'houseNo': '', - }, - 'company': delivery_address.address_title, - 'person': { - 'title': delivery_contact.title, - 'firstname': delivery_contact.first_name, - 'lastname': delivery_contact.last_name - }, - 'phone': { - 'phoneNumber': delivery_contact.phone, - 'phoneNumberPrefix': delivery_contact.phone_prefix - }, - 'email': delivery_contact.email, - }, - 'shipmentDetails': { - 'contentDescription': description_of_content, - 'shipmentType': 'PARCEL', - 'shipmentSettings': { - 'saturdayDelivery': False, - 'ddp': False, - 'insurance': False, - 'pickupOrder': False, - 'pickupTailLift': False, - 'deliveryTailLift': False, - 'holidayDelivery': False, - }, - 'goodsValue': value_of_goods, - 'parcelList': parcel_list, - 'pickupInterval': { - 'date': pickup_date - } - } - } - - if service_info: - payload['service'] = { - 'baseServiceDetails': { - 'id': service_info['id'], - 'name': service_info['service_name'], - 'carrier': service_info['carrier'], - 'priceInfo': service_info['price_info'], - }, - 'supportedExWorkType': [], - 'messages': [''], - 'description': '', - 'serviceInfo': '', - } - payload['shipmentNotification'] = { - 'trackingNotification': { - 'deliveryNotification': True, - 'problemNotification': True, - 'emails': [tracking_notific_email], - 'notificationText': '', - }, - 'recipientNotification': { - 'notificationText': '', - 'emails': [ shipment_notific_email ] - } - } - payload['labelEmail'] = True - return payload - -def get_letmeship_label(shipment_id): - try: - # Retrieve shipment label from LetMeShip - api_id = frappe.db.get_single_value('LetMeShip','api_id') - api_password = frappe.db.get_single_value('LetMeShip','api_password') - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Access-Control-Allow-Origin': 'string' - } - url = 'https://api.letmeship.com/v1/shipments/{id}/documents?types=LABEL'\ - .format(id=shipment_id) - shipment_label_response = requests.get( - url, - auth=(api_id,api_password), - headers=headers - ) - shipment_label_response_data = json.loads(shipment_label_response.text) - if 'documents' in shipment_label_response_data: - for label in shipment_label_response_data['documents']: - if 'data' in label: - return json.dumps(label['data']) - else: - frappe.throw( - _('Error occurred while printing Shipment: {0}') - .format(shipment_label_response_data['message']) - ) - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint( - _('Error occurred while printing Shipment: {0}') - .format(str(exc)), - indicator='orange', - alert=True - ) - - -def get_letmeship_tracking_data(shipment_id): - # return letmeship tracking data - api_id = frappe.db.get_single_value('LetMeShip','api_id') - api_password = frappe.db.get_single_value('LetMeShip','api_password') - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Access-Control-Allow-Origin': 'string' - } - try: - url = 'https://api.letmeship.com/v1/tracking?shipmentid={id}'.format(id=shipment_id) - tracking_data_response = requests.get( - url, - auth=(api_id, api_password), - headers=headers - ) - tracking_data = json.loads(tracking_data_response.text) - if 'awbNumber' in tracking_data: - tracking_status = 'In Progress' - if tracking_data['lmsTrackingStatus'].startswith('DELIVERED'): - tracking_status = 'Delivered' - if tracking_data['lmsTrackingStatus'] == 'RETURNED': - tracking_status = 'Returned' - if tracking_data['lmsTrackingStatus'] == 'LOST': - tracking_status = 'Lost' - tracking_url = get_tracking_url( - carrier=tracking_data['carrier'], - tracking_number=tracking_data['awbNumber'] - ) - return { - 'awb_number': tracking_data['awbNumber'], - 'tracking_status': tracking_status, - 'tracking_status_info': tracking_data['lmsTrackingStatus'], - 'tracking_url': tracking_url, - } - elif 'message' in tracking_data: - frappe.throw( - _('Error occurred while updating Shipment: {0}') - .format(tracking_data['message']) - ) - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint( - _('Error occurred while updating Shipment: {0}') - .format(str(exc)), - indicator='orange', - alert=True - ) - - -def get_parcel_list(shipment_parcel, description_of_content): - parcel_list = [] - for parcel in shipment_parcel: - formatted_parcel = {} - formatted_parcel['height'] = parcel.get('height') - formatted_parcel['width'] = parcel.get('width') - formatted_parcel['length'] = parcel.get('length') - formatted_parcel['weight'] = parcel.get('weight') - formatted_parcel['quantity'] = parcel.get('count') - formatted_parcel['contentDescription'] = description_of_content - parcel_list.append(formatted_parcel) - return parcel_list - -def set_letmeship_specific_fields(pickup_contact, delivery_contact): - pickup_contact.phone_prefix = pickup_contact.phone[:3] - pickup_contact.phone = re.sub('[^A-Za-z0-9]+', '', pickup_contact.phone[3:]) - - pickup_contact.title = 'MS' - if pickup_contact.gender == 'Male': - pickup_contact.title = 'MR' - - delivery_contact.phone_prefix = delivery_contact.phone[:3] - delivery_contact.phone = re.sub('[^A-Za-z0-9]+', '', delivery_contact.phone[3:]) - - delivery_contact.title = 'MS' - if delivery_contact.gender == 'Male': - delivery_contact.title = 'MR' \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py b/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py deleted file mode 100644 index 3439e4fd72..0000000000 --- a/erpnext/erpnext_integrations/doctype/letmeship/test_letmeship.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestLetMeShip(unittest.TestCase): - pass diff --git a/erpnext/erpnext_integrations/doctype/packlink/__init__.py b/erpnext/erpnext_integrations/doctype/packlink/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.js b/erpnext/erpnext_integrations/doctype/packlink/packlink.js deleted file mode 100644 index da864584f6..0000000000 --- a/erpnext/erpnext_integrations/doctype/packlink/packlink.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Packlink', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.json b/erpnext/erpnext_integrations/doctype/packlink/packlink.json deleted file mode 100644 index a56595e9a1..0000000000 --- a/erpnext/erpnext_integrations/doctype/packlink/packlink.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "actions": [], - "creation": "2020-07-22 10:45:17.672439", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enabled", - "api_key" - ], - "fields": [ - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - }, - { - "fieldname": "api_key", - "fieldtype": "Data", - "label": "API Key", - "read_only_depends_on": "eval:doc.enabled == 0" - } - ], - "issingle": 1, - "links": [], - "modified": "2020-08-05 16:33:59.720980", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "Packlink", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/packlink/packlink.py b/erpnext/erpnext_integrations/doctype/packlink/packlink.py deleted file mode 100644 index 1db08c3149..0000000000 --- a/erpnext/erpnext_integrations/doctype/packlink/packlink.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import json -import frappe -import requests -from frappe import _ -from frappe.model.document import Document -from erpnext.erpnext_integrations.utils import get_tracking_url - -PACKLINK_PROVIDER = 'Packlink' - -class Packlink(Document): - pass - -def get_packlink_available_services(pickup_address, delivery_address, shipment_parcel,pickup_date): - # Retrieve rates at PackLink from specification stated. - from_zip = pickup_address.pincode - from_country_code = pickup_address.country_code - to_zip = delivery_address.pincode - to_country_code = delivery_address.country_code - shipment_parcel_params = '' - parcel_list = packlink_get_parcel_list(json.loads(shipment_parcel)) - for (index, parcel) in enumerate(parcel_list): - shipment_parcel_params += 'packages[{index}][height]={height}&packages[{index}][length]={length}&packages[{index}][weight]={weight}&packages[{index}][width]={width}&'.format( - index=index, - height=parcel['height'], - length=parcel['length'], - weight=parcel['weight'], - width=parcel['width'] - ) - url = 'https://api.packlink.com/v1/services?from[country]={}&from[zip]={}&to[country]={}&to[zip]={}&{}sortBy=totalPrice&source=PRO'.format( - from_country_code, - from_zip, - to_country_code, - to_zip, - shipment_parcel_params - ) - api_key = frappe.db.get_single_value('Packlink', 'api_key') - enabled = frappe.db.get_single_value('Packlink', 'enabled') - if not api_key or not enabled: - return [] - try: - responses = requests.get(url, headers={'Authorization': api_key}) - responses_dict = json.loads(responses.text) - # If an error occured on the api. Show the error message - if 'messages' in responses_dict: - frappe.msgprint( - _('Packlink: {0}' - .format(str(responses_dict['messages'][0]['message'])) - ), - indicator='orange', - alert=True - ) - available_services = [] - for response in responses_dict: - if parse_pickup_date(pickup_date) \ - in response['available_dates'].keys(): - available_service = frappe._dict() - available_service.service_provider = PACKLINK_PROVIDER - available_service.carrier = response['carrier_name'] - available_service.carrier_name = response['name'] - available_service.service_name = '' - available_service.is_preferred = 0 - available_service.total_price = response['price']['base_price'] - available_service.actual_price = response['price']['total_price'] - available_service.service_id = response['id'] - available_service.available_dates = response['available_dates'] - available_services.append(available_service) - - return available_services - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint( - _('Error occurred on Packlink: {0}') - .format(str(exc)), indicator='orange', - alert=True - ) - return [] - - -def create_packlink_shipment(pickup_address, delivery_address, shipment_parcel, - description_of_content, pickup_date, value_of_goods, pickup_contact, - delivery_contact, service_info): - # Create a transaction at PackLink - enabled = frappe.db.get_single_value('Packlink', 'enabled') - if not enabled: - frappe.throw(_('Packlink integration is not enabled')) - api_key = frappe.db.get_single_value('Packlink', 'api_key') - from_country_code = pickup_address.country_code - to_country_code = delivery_address.country_code - data = { - 'additional_data': { - 'postal_zone_id_from': '', - 'postal_zone_name_from': pickup_address.country, - 'postal_zone_id_to': '', - 'postal_zone_name_to': delivery_address.country, - }, - 'collection_date': parse_pickup_date(pickup_date), - 'collection_time': '', - 'content': description_of_content, - 'contentvalue': value_of_goods, - 'content_second_hand': False, - 'from': { - 'city': pickup_address.city, - 'company': pickup_address.address_title, - 'country': from_country_code, - 'email': pickup_contact.email, - 'name': pickup_contact.first_name, - 'phone': pickup_contact.phone, - 'state': pickup_address.country, - 'street1': pickup_address.address_line1, - 'street2': pickup_address.address_line2, - 'surname': pickup_contact.last_name, - 'zip_code': pickup_address.pincode, - }, - 'insurance': {'amount': 0, 'insurance_selected': False}, - 'price': {}, - 'packages': packlink_get_parcel_list(json.loads(shipment_parcel)), - 'service_id': service_info['service_id'], - 'to': { - 'city': delivery_address.city, - 'company': delivery_address.address_title, - 'country': to_country_code, - 'email': delivery_contact.email, - 'name': delivery_contact.first_name, - 'phone': delivery_contact.phone, - 'state': delivery_address.country, - 'street1': delivery_address.address_line1, - 'street2': delivery_address.address_line2, - 'surname': delivery_contact.last_name, - 'zip_code': delivery_address.pincode, - }, - } - - url = 'https://api.packlink.com/v1/shipments' - headers = { - 'Authorization': api_key, - 'Content-Type': 'application/json' - } - try: - response_data = requests.post(url, json=data, headers=headers) - response_data = json.loads(response_data.text) - if 'reference' in response_data: - return { - 'service_provider': PACKLINK_PROVIDER, - 'shipment_id': response_data['reference'], - 'carrier': service_info['carrier'], - 'carrier_service': service_info['service_name'], - 'shipment_amount': service_info['actual_price'], - 'awb_number': '', - } - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint( - _('Error occurred while creating Shipment: {0}') - .format(str(exc)), - indicator='orange', - alert=True - ) - - -def get_packlink_label(shipment_id): - # Retrieve shipment label from PackLink - enabled = frappe.db.get_single_value('Packlink', 'enabled') - if not enabled: - frappe.throw(_('Packlink integration is not enabled')) - api_key = frappe.db.get_single_value('Packlink', 'api_key') - headers = { - 'Authorization': api_key, - 'Content-Type': 'application/json' - } - shipment_label_response = requests.get( - 'https://api.packlink.com/v1/shipments/{id}/labels'.format(id=shipment_id), - headers=headers - ) - shipment_label = json.loads(shipment_label_response.text) - if shipment_label: - return shipment_label - else: - frappe.msgprint(_('Shipment ID not found')) - - -def get_packlink_tracking_data(shipment_id): - # Get Packlink Tracking Info - enabled = frappe.db.get_single_value('Packlink', 'enabled') - if not enabled: - frappe.throw(_('Packlink integration is not enabled')) - api_key = frappe.db.get_single_value('Packlink', 'api_key') - headers = { - 'Authorization': api_key, - 'Content-Type': 'application/json' - } - try: - url = 'https://api.packlink.com/v1/shipments/{id}'.format(id=shipment_id) - tracking_data_response = requests.get(url, headers=headers) - tracking_data = json.loads(tracking_data_response.text) - if 'trackings' in tracking_data: - tracking_status = 'In Progress' - if tracking_data['state'] == 'DELIVERED': - tracking_status = 'Delivered' - if tracking_data['state'] == 'RETURNED': - tracking_status = 'Returned' - if tracking_data['state'] == 'LOST': - tracking_status = 'Lost' - awb_number = None if not tracking_data['trackings'] else tracking_data['trackings'][0] - tracking_url = get_tracking_url( - carrier=tracking_data['carrier'], - tracking_number=awb_number - ) - return { - 'awb_number': awb_number, - 'tracking_status': tracking_status, - 'tracking_status_info': tracking_data['state'], - 'tracking_url': tracking_url - } - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint(_('Error occurred while updating Shipment: {0}').format( - str(exc)), indicator='orange', alert=True) - return [] - - -def packlink_get_parcel_list(shipment_parcel): - parcel_list = [] - for parcel in shipment_parcel: - for count in range(parcel.get('count')): - formatted_parcel = {} - formatted_parcel['height'] = parcel.get('height') - formatted_parcel['width'] = parcel.get('width') - formatted_parcel['length'] = parcel.get('length') - formatted_parcel['weight'] = parcel.get('weight') - parcel_list.append(formatted_parcel) - return parcel_list - - -def parse_pickup_date(pickup_date): - return pickup_date.replace('-', '/') \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py b/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py deleted file mode 100644 index 106ae51f7c..0000000000 --- a/erpnext/erpnext_integrations/doctype/packlink/test_packlink.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestPacklink(unittest.TestCase): - pass diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py b/erpnext/erpnext_integrations/doctype/sendcloud/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js deleted file mode 100644 index 3b85236863..0000000000 --- a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('SendCloud', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json deleted file mode 100644 index 37b6898cba..0000000000 --- a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "actions": [], - "creation": "2020-08-18 09:48:50.836233", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enabled", - "api_key", - "api_secret" - ], - "fields": [ - { - "default": "0", - "fieldname": "enabled", - "fieldtype": "Check", - "label": "Enabled" - }, - { - "fieldname": "api_key", - "fieldtype": "Data", - "label": "API Key", - "read_only_depends_on": "eval:doc.enabled == 0" - }, - { - "fieldname": "api_secret", - "fieldtype": "Password", - "label": "API Secret", - "read_only_depends_on": "eval:doc.enabled == 0" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2020-10-21 10:28:57.710549", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "SendCloud", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py deleted file mode 100644 index d30af15eb5..0000000000 --- a/erpnext/erpnext_integrations/doctype/sendcloud/sendcloud.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import requests -import frappe -import json -from frappe import _ -from frappe.model.document import Document - -SENDCLOUD_PROVIDER = 'SendCloud' - -class SendCloud(Document): - pass - -def get_sendcloud_available_services(delivery_address, shipment_parcel): - # Retrieve rates at SendCloud from specification stated. - api_key, api_secret, enabled = frappe.db.get_value('SendCloud', 'SendCloud', ['enabled', 'api_key', 'api_secret']) - if not enabled or not api_key or not api_secret: - return [] - - try: - url = 'https://panel.sendcloud.sc/api/v2/shipping_methods' - responses = requests.get(url, auth=(api_key, api_secret)) - responses_dict = json.loads(responses.text) - - available_services = [] - for service in responses_dict['shipping_methods']: - for country in service['countries']: - if country['iso_2'] == delivery_address.country_code: - available_service = frappe._dict() - available_service.service_provider = 'SendCloud' - available_service.carrier = service['carrier'] - available_service.service_name = service['name'] - available_service.total_price = total_parcel_price(country['price'], json.loads(shipment_parcel)) - available_service.service_id = service['id'] - available_services.append(available_service) - return available_services - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint(_('Error occurred on SendCloud: {0}').format( - str(exc)), indicator='orange', alert=True) - -def create_sendcloud_shipment( - shipment, - delivery_address, - delivery_contact, - service_info, - shipment_parcel, - description_of_content, - value_of_goods -): - # Create a transaction at SendCloud - api_key, api_secret, enabled = frappe.db.get_value('SendCloud', 'SendCloud', ['enabled', 'api_key', 'api_secret']) - if not enabled or not api_key or not api_secret: - return [] - - parcels = [] - for i, parcel in enumerate(json.loads(shipment_parcel), start=1): - parcel_data = { - 'name': "{} {}".format(delivery_contact.first_name, delivery_contact.last_name), - 'company_name': delivery_address.address_title, - 'address': delivery_address.address_line1, - 'address_2': delivery_address.address_line2 or '', - 'city': delivery_address.city, - 'postal_code': delivery_address.pincode, - 'telephone': delivery_contact.phone, - 'request_label': True, - 'email': delivery_contact.email, - 'data': [], - 'country': delivery_address.country_code, - 'shipment': { - 'id': service_info['service_id'] - }, - 'order_number': "{}-{}".format(shipment, i), - 'external_reference': "{}-{}".format(shipment, i), - 'weight': parcel.get('weight'), - 'parcel_items': get_parcel_items(parcel, description_of_content, value_of_goods) - } - parcels.append(parcel_data) - data = { - 'parcels': parcels - } - try: - url = 'https://panel.sendcloud.sc/api/v2/parcels?errors=verbose' - response_data = requests.post(url, json=data, auth=(api_key, api_secret)) - response_data = json.loads(response_data.text) - if 'failed_parcels' in response_data: - frappe.msgprint(_('Error occurred while creating Shipment: {0}' - ).format(response_data['failed_parcels'][0]['errors']), indicator='orange', - alert=True) - else: - shipment_id = ', '.join([str(x['id']) for x in response_data['parcels']]) - awb_number = ', '.join([str(x['tracking_number']) for x in response_data['parcels']]) - return { - 'service_provider': 'SendCloud', - 'shipment_id': shipment_id, - 'carrier': service_info['carrier'], - 'carrier_service': service_info['service_name'], - 'shipment_amount': service_info['total_price'], - 'awb_number': awb_number - } - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint(_('Error occurred while creating Shipment: {0}').format( - str(exc)), indicator='orange', alert=True) - -def get_sendcloud_label(shipment_id): - # Retrieve shipment label from SendCloud - api_key = frappe.db.get_single_value('SendCloud', 'api_key') - api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') - shipment_id_list = shipment_id.split(', ') - label_urls = [] - for ship_id in shipment_id_list: - shipment_label_response = \ - requests.get('https://panel.sendcloud.sc/api/v2/labels/{id}'.format(id=ship_id), auth=(api_key, api_secret)) - shipment_label = json.loads(shipment_label_response.text) - label_urls.append(shipment_label['label']['label_printer']) - if len(label_urls): - return label_urls - else: - frappe.msgprint(_('Shipment ID not found')) - -def get_sendcloud_tracking_data(shipment_id): - # return SendCloud tracking data - try: - api_key = frappe.db.get_single_value('SendCloud', 'api_key') - api_secret = frappe.db.get_single_value('SendCloud', 'api_secret') - shipment_id_list = shipment_id.split(', ') - awb_number = [] - tracking_status = [] - tracking_status_info = [] - tracking_urls = [] - for ship_id in shipment_id_list: - tracking_data_response = \ - requests.get('https://panel.sendcloud.sc/api/v2/parcels/{id}'.format(id=ship_id), auth=(api_key, api_secret)) - tracking_data = json.loads(tracking_data_response.text) - tracking_urls.append(tracking_data['parcel']['tracking_url']) - awb_number.append(tracking_data['parcel']['tracking_number']) - tracking_status.append(tracking_data['parcel']['status']['message']) - tracking_status_info.append(tracking_data['parcel']['status']['message']) - return { - 'awb_number': ', '.join(awb_number), - 'tracking_status': ', '.join(tracking_status), - 'tracking_status_info': ', '.join(tracking_status_info), - 'tracking_url': ', '.join(tracking_urls) - } - except Exception as exc: - frappe.log_error(frappe.get_traceback()) - frappe.msgprint(_('Error occurred while updating Shipment: {0}').format( - str(exc)), indicator='orange', alert=True) - -def total_parcel_price(parcel_price, shipment_parcel): - count = 0 - for parcel in shipment_parcel: - count += parcel.get('count') - return parcel_price * count - -def get_parcel_items(parcel, description_of_content, value_of_goods): - parcel_list = [] - formatted_parcel = {} - formatted_parcel['description'] = description_of_content - formatted_parcel['quantity'] = parcel.get('count') - formatted_parcel['weight'] = parcel.get('weight') - formatted_parcel['value'] = value_of_goods - parcel_list.append(formatted_parcel) - return parcel_list \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py b/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py deleted file mode 100644 index 5cbe80e8ac..0000000000 --- a/erpnext/erpnext_integrations/doctype/sendcloud/test_sendcloud.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestSendCloud(unittest.TestCase): - pass diff --git a/erpnext/stock/doctype/parcel_service/__init__.py b/erpnext/stock/doctype/parcel_service/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.js b/erpnext/stock/doctype/parcel_service/parcel_service.js deleted file mode 100644 index 43b8ed5bf8..0000000000 --- a/erpnext/stock/doctype/parcel_service/parcel_service.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Parcel Service', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.json b/erpnext/stock/doctype/parcel_service/parcel_service.json deleted file mode 100644 index 9960acf4ae..0000000000 --- a/erpnext/stock/doctype/parcel_service/parcel_service.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "field:parcel_service_name", - "creation": "2020-07-23 10:35:38.211715", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "parcel_service_name", - "parcel_service_code", - "url_reference" - ], - "fields": [ - { - "fieldname": "parcel_service_name", - "fieldtype": "Data", - "label": "Parcel Service Name", - "unique": 1 - }, - { - "fieldname": "parcel_service_code", - "fieldtype": "Data", - "label": "Parcel Service Code" - }, - { - "fieldname": "url_reference", - "fieldtype": "Data", - "label": "URL Reference" - } - ], - "links": [], - "modified": "2020-07-23 10:35:38.211715", - "modified_by": "Administrator", - "module": "Stock", - "name": "Parcel Service", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/stock/doctype/parcel_service/parcel_service.py b/erpnext/stock/doctype/parcel_service/parcel_service.py deleted file mode 100644 index e46ac76ef7..0000000000 --- a/erpnext/stock/doctype/parcel_service/parcel_service.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class ParcelService(Document): - pass diff --git a/erpnext/stock/doctype/parcel_service/test_parcel_service.py b/erpnext/stock/doctype/parcel_service/test_parcel_service.py deleted file mode 100644 index c2f96d9cb0..0000000000 --- a/erpnext/stock/doctype/parcel_service/test_parcel_service.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestParcelService(unittest.TestCase): - pass diff --git a/erpnext/stock/doctype/parcel_service_type/__init__.py b/erpnext/stock/doctype/parcel_service_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js deleted file mode 100644 index 31d54536c0..0000000000 --- a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Parcel Service Type Alias', { - parcel_type_alias: function(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - if (row.parcel_type_alias) { - frappe.model.set_value(cdt, cdn, 'parcel_service', frm.doc.parcel_service); - frm.refresh_field('parcel_service_type_alias'); - } - } -}); diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json deleted file mode 100644 index 3c0c4d5f80..0000000000 --- a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "format: {parcel_service} - {parcel_service_type}", - "creation": "2020-07-23 10:47:43.794083", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "parcel_service", - "parcel_service_type", - "description", - "section_break_4", - "parcel_service_type_alias", - "column_break_6", - "section_break_7", - "show_in_preferred_services_list" - ], - "fields": [ - { - "fieldname": "parcel_service", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Parcel Service", - "options": "Parcel Service", - "reqd": 1 - }, - { - "fieldname": "parcel_service_type", - "fieldtype": "Data", - "label": "Parcel Service Type", - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "label": "Description" - }, - { - "fieldname": "section_break_4", - "fieldtype": "Section Break" - }, - { - "fieldname": "parcel_service_type_alias", - "fieldtype": "Table", - "label": "Parcel Service Type Alias", - "options": "Parcel Service Type Alias" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "default": "0", - "fieldname": "show_in_preferred_services_list", - "fieldtype": "Check", - "label": "Show in Preferred Services List" - } - ], - "links": [], - "modified": "2020-07-23 10:47:43.794083", - "modified_by": "Administrator", - "module": "Stock", - "name": "Parcel Service Type", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py b/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py deleted file mode 100644 index b55528c359..0000000000 --- a/erpnext/stock/doctype/parcel_service_type/parcel_service_type.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class ParcelServiceType(Document): - pass - -def match_parcel_service_type_alias(parcel_service_type, parcel_service): - # Match and return Parcel Service Type Alias to Parcel Service Type if exists. - if frappe.db.exists('Parcel Service', parcel_service): - matched_parcel_service_type = \ - frappe.db.get_value('Parcel Service Type Alias', { - 'parcel_type_alias': parcel_service_type, - 'parcel_service': parcel_service - }, 'parent') - if matched_parcel_service_type: - parcel_service_type = matched_parcel_service_type - return parcel_service_type diff --git a/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py b/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py deleted file mode 100644 index e214264acc..0000000000 --- a/erpnext/stock/doctype/parcel_service_type/test_parcel_service_type.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestParcelServiceType(unittest.TestCase): - pass diff --git a/erpnext/stock/doctype/parcel_service_type_alias/__init__.py b/erpnext/stock/doctype/parcel_service_type_alias/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json deleted file mode 100644 index 8e7731e6c1..0000000000 --- a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "actions": [], - "creation": "2020-07-23 10:47:23.626510", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "parcel_service", - "parcel_type_alias" - ], - "fields": [ - { - "fieldname": "parcel_service", - "fieldtype": "Link", - "hidden": 1, - "in_list_view": 1, - "label": "Parcel Service", - "options": "Parcel Service", - "read_only": 1 - }, - { - "fieldname": "parcel_type_alias", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Parcel Type Alias", - "reqd": 1 - } - ], - "istable": 1, - "links": [], - "modified": "2020-07-23 10:47:23.626510", - "modified_by": "Administrator", - "module": "Stock", - "name": "Parcel Service Type Alias", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py b/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py deleted file mode 100644 index fd0a7d8b49..0000000000 --- a/erpnext/stock/doctype/parcel_service_type_alias/parcel_service_type_alias.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class ParcelServiceTypeAlias(Document): - pass diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index fc0b05f8af..aa792a4883 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -114,26 +114,6 @@ frappe.ui.form.on('Shipment', { }); }, refresh: function(frm) { - if (frm.doc.docstatus === 1 && !frm.doc.shipment_id) { - frm.add_custom_button(__('Fetch Shipping Rates'), function() { - return frm.events.fetch_shipping_rates(frm); - }); - } - if (frm.doc.shipment_id) { - frm.add_custom_button(__('Print Shipping Label'), function() { - return frm.events.print_shipping_label(frm); - }, __('Tools')); - if (frm.doc.tracking_status != 'Delivered') { - frm.add_custom_button(__('Update Tracking'), function() { - return frm.events.update_tracking(frm, frm.doc.service_provider, frm.doc.shipment_id); - }, __('Tools')); - - frm.add_custom_button(__('Track Status'), function() { - const urls = frm.doc.tracking_url.split(', '); - urls.forEach(url => window.open(url)); - }, __('View')); - } - } $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); @@ -598,90 +578,6 @@ frappe.ui.form.on('Shipment', { frm.refresh_fields("pickup_from_send_shipping_notification"); frm.refresh_fields("pickup_from_subscribe_to_status_updates"); } - }, - fetch_shipping_rates: function(frm) { - if (!frm.doc.shipment_id) { - frappe.call({ - method: "erpnext.stock.doctype.shipment.shipment.fetch_shipping_rates", - freeze: true, - freeze_message: __("Fetching Shipping Rates"), - args: { - pickup_from_type: frm.doc.pickup_from_type, - delivery_to_type: frm.doc.delivery_to_type, - pickup_address_name: frm.doc.pickup_address_name, - delivery_address_name: frm.doc.delivery_address_name, - shipment_parcel: frm.doc.shipment_parcel, - description_of_content: frm.doc.description_of_content, - pickup_date: frm.doc.pickup_date, - pickup_contact_name: frm.doc.pickup_from_type === 'Company' ? frm.doc.pickup_contact_person : frm.doc.pickup_contact_name, - delivery_contact_name: frm.doc.delivery_contact_name, - value_of_goods: frm.doc.value_of_goods - }, - callback: function(r) { - if (r.message) { - select_from_available_services(frm, r.message); - } - else { - frappe.throw(__("No Shipment Services available")); - } - } - }); - } - else { - frappe.throw(__("Shipment already created")); - } - }, - print_shipping_label: function(frm) { - frappe.call({ - method: "erpnext.stock.doctype.shipment.shipment.print_shipping_label", - freeze: true, - freeze_message: __("Printing Shipping Label"), - args: { - shipment_id: frm.doc.shipment_id, - service_provider: frm.doc.service_provider - }, - callback: function(r) { - if (r.message) { - if (frm.doc.service_provider == "LetMeShip") { - var array = JSON.parse(r.message); - // Uint8Array for unsigned bytes - array = new Uint8Array(array); - const file = new Blob([array], {type: "application/pdf"}); - const file_url = URL.createObjectURL(file); - window.open(file_url); - } - else { - if (Array.isArray(r.message)) { - r.message.forEach(url => window.open(url)); - } else { - window.open(r.message); - } - } - } - } - }); - }, - update_tracking: function(frm, service_provider, shipment_id) { - let delivery_notes = []; - (frm.doc.shipment_delivery_note || []).forEach((d) => { - delivery_notes.push(d.delivery_note); - }); - frappe.call({ - method: "erpnext.stock.doctype.shipment.shipment.update_tracking", - freeze: true, - freeze_message: __("Updating Tracking"), - args: { - shipment: frm.doc.name, - shipment_id: shipment_id, - service_provider: service_provider, - delivery_notes: delivery_notes - }, - callback: function(r) { - if (!r.exc) { - frm.reload_doc(); - } - } - }); } }); @@ -712,82 +608,3 @@ var validate_duplicate = function(frm, table, fieldname, index){ : frm.doc[table].some((detail, i) => detail.email === fieldname && !(index === i)) ); }; - -function select_from_available_services(frm, available_services) { - var headers = [ __("Service Provider"), __("Carrier"), __("Carrier’s Service"), __("Price"), "" ]; - cur_frm.render_available_services = function(d, headers, data){ - const arranged_data = data.reduce((prev, curr) => { - if (curr.is_preferred) { - prev.preferred_services.push(curr); - } else { - prev.other_services.push(curr); - } - return prev; - }, { preferred_services: [], other_services: [] }); - d.fields_dict.available_services.$wrapper.html( - frappe.render_template('shipment_service_selector', - {'header_columns': headers, 'data': arranged_data} - ) - ); - }; - const d = new frappe.ui.Dialog({ - title: __("Select Shipment Service to create Shipment"), - fields: [ - { - fieldtype:'HTML', - fieldname:"available_services", - label: __('Available Services') - } - ] - }); - cur_frm.render_available_services(d, headers, available_services); - let shipment_notific_email = []; - let tracking_notific_email = []; - (frm.doc.shipment_notification_subscription || []).forEach((d) => { - if (!d.unsubscribed) { - shipment_notific_email.push(d.email); - } - }); - (frm.doc.shipment_status_update_subscription || []).forEach((d) => { - if (!d.unsubscribed) { - tracking_notific_email.push(d.email); - } - }); - let delivery_notes = []; - (frm.doc.shipment_delivery_note || []).forEach((d) => { - delivery_notes.push(d.delivery_note); - }); - cur_frm.select_row = function(service_data){ - frappe.call({ - method: "erpnext.stock.doctype.shipment.shipment.create_shipment", - freeze: true, - freeze_message: __("Creating Shipment"), - args: { - shipment: frm.doc.name, - pickup_from_type: frm.doc.pickup_from_type, - delivery_to_type: frm.doc.delivery_to_type, - pickup_address_name: frm.doc.pickup_address_name, - delivery_address_name: frm.doc.delivery_address_name, - shipment_parcel: frm.doc.shipment_parcel, - description_of_content: frm.doc.description_of_content, - pickup_date: frm.doc.pickup_date, - pickup_contact_name: frm.doc.pickup_from_type === 'Company' ? frm.doc.pickup_contact_person : frm.doc.pickup_contact_name, - delivery_contact_name: frm.doc.delivery_contact_name, - value_of_goods: frm.doc.value_of_goods, - service_data: service_data, - shipment_notific_email: shipment_notific_email, - tracking_notific_email: tracking_notific_email, - delivery_notes: delivery_notes - }, - callback: function(r) { - if (!r.exc) { - frm.reload_doc(); - frappe.msgprint(__("Shipment created with {0}, ID is {1}", [r.message.service_provider, r.message.shipment_id])); - frm.events.update_tracking(frm, r.message.service_provider, r.message.shipment_id); - } - } - }); - d.hide(); - }; - d.show(); -} diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index bbfbb719be..9ac6102ded 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -358,28 +358,24 @@ { "fieldname": "service_provider", "fieldtype": "Data", - "label": "Service Provider", - "read_only": 1 + "label": "Service Provider" }, { "fieldname": "shipment_id", "fieldtype": "Data", - "label": "Shipment ID", - "read_only": 1 + "label": "Shipment ID" }, { "fieldname": "shipment_amount", "fieldtype": "Currency", "label": "Shipment Amount", - "precision": "2", - "read_only": 1 + "precision": "2" }, { "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", - "read_only": 1 + "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted" }, { "fieldname": "tracking_url", @@ -391,27 +387,23 @@ { "fieldname": "carrier", "fieldtype": "Data", - "label": "Carrier", - "read_only": 1 + "label": "Carrier" }, { "fieldname": "carrier_service", "fieldtype": "Data", - "label": "Carrier Service", - "read_only": 1 + "label": "Carrier Service" }, { "fieldname": "awb_number", "fieldtype": "Data", - "label": "AWB Number", - "read_only": 1 + "label": "AWB Number" }, { "fieldname": "tracking_status", "fieldtype": "Select", "label": "Tracking Status", - "options": "\nIn Progress\nDelivered\nReturned\nLost", - "read_only": 1 + "options": "\nIn Progress\nDelivered\nReturned\nLost" }, { "fieldname": "tracking_status_info", @@ -468,7 +460,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-09-29 13:59:50.241744", + "modified": "2020-11-20 16:19:06.157106", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 9b3c976ca4..4e16f95533 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -10,10 +10,6 @@ from frappe.utils import flt from frappe.model.document import Document from erpnext.accounts.party import get_party_shipping_address from frappe.contacts.doctype.contact.contact import get_default_contact -from erpnext.erpnext_integrations.doctype.letmeship.letmeship import LETMESHIP_PROVIDER, get_letmeship_available_services, create_letmeship_shipment, get_letmeship_label, get_letmeship_tracking_data -from erpnext.erpnext_integrations.doctype.packlink.packlink import PACKLINK_PROVIDER, get_packlink_available_services, create_packlink_shipment, get_packlink_label, get_packlink_tracking_data -from erpnext.erpnext_integrations.doctype.sendcloud.sendcloud import SENDCLOUD_PROVIDER, get_sendcloud_available_services, create_sendcloud_shipment, get_sendcloud_label, get_sendcloud_tracking_data -from erpnext.stock.doctype.parcel_service_type.parcel_service_type import match_parcel_service_type_alias class Shipment(Document): def validate(self): @@ -36,159 +32,6 @@ class Shipment(Document): if flt(parcel.weight) <= 0: frappe.throw(_('Parcel weight cannot be 0')) -@frappe.whitelist() -def fetch_shipping_rates(pickup_from_type, delivery_to_type, pickup_address_name, delivery_address_name, - shipment_parcel, description_of_content, pickup_date, value_of_goods, - pickup_contact_name=None, delivery_contact_name=None): - # Return Shipping Rates for the various Shipping Providers - shipment_prices = [] - letmeship_enabled = frappe.db.get_single_value('LetMeShip','enabled') - packlink_enabled = frappe.db.get_single_value('Packlink','enabled') - sendcloud_enabled = frappe.db.get_single_value('SendCloud','enabled') - pickup_address = get_address(pickup_address_name) - delivery_address = get_address(delivery_address_name) - if letmeship_enabled: - pickup_contact = None - delivery_contact = None - if pickup_from_type != 'Company': - pickup_contact = get_contact(pickup_contact_name) - else: - pickup_contact = get_company_contact(user=pickup_contact_name) - - if delivery_to_type != 'Company': - delivery_contact = get_contact(delivery_contact_name) - else: - delivery_contact = get_company_contact(user=pickup_contact_name) - letmeship_prices = get_letmeship_available_services( - delivery_to_type=delivery_to_type, - pickup_address=pickup_address, - delivery_address=delivery_address, - shipment_parcel=shipment_parcel, - description_of_content=description_of_content, - pickup_date=pickup_date, - value_of_goods=value_of_goods, - pickup_contact=pickup_contact, - delivery_contact=delivery_contact, - ) - letmeship_prices = match_parcel_service_type_carrier(letmeship_prices, ['carrier', 'carrier_name']) - shipment_prices = shipment_prices + letmeship_prices - if packlink_enabled: - packlink_prices = get_packlink_available_services( - pickup_address=pickup_address, - delivery_address=delivery_address, - shipment_parcel=shipment_parcel, - pickup_date=pickup_date - ) - packlink_prices = match_parcel_service_type_carrier(packlink_prices, ['carrier_name', 'carrier']) - shipment_prices = shipment_prices + packlink_prices - if sendcloud_enabled and pickup_from_type == 'Company': - sendcloud_prices = get_sendcloud_available_services( - delivery_address=delivery_address, - shipment_parcel=shipment_parcel - ) - shipment_prices = shipment_prices + sendcloud_prices - shipment_prices = sorted(shipment_prices, key=lambda k:k['total_price']) - return shipment_prices - -@frappe.whitelist() -def create_shipment(shipment, pickup_from_type, delivery_to_type, pickup_address_name, - delivery_address_name, shipment_parcel, description_of_content, pickup_date, - value_of_goods, service_data, shipment_notific_email, tracking_notific_email, - pickup_contact_name=None, delivery_contact_name=None, delivery_notes=[]): - # Create Shipment for the selected provider - service_info = json.loads(service_data) - shipment_info = None - pickup_contact = None - delivery_contact = None - pickup_address = get_address(pickup_address_name) - delivery_address = get_address(delivery_address_name) - if pickup_from_type != 'Company': - pickup_contact = get_contact(pickup_contact_name) - else: - pickup_contact = get_company_contact(user=pickup_contact_name) - - if delivery_to_type != 'Company': - delivery_contact = get_contact(delivery_contact_name) - else: - delivery_contact = get_company_contact(user=pickup_contact_name) - if service_info['service_provider'] == LETMESHIP_PROVIDER: - shipment_info = create_letmeship_shipment( - pickup_address=pickup_address, - delivery_address=delivery_address, - shipment_parcel=shipment_parcel, - description_of_content=description_of_content, - pickup_date=pickup_date, - value_of_goods=value_of_goods, - pickup_contact=pickup_contact, - delivery_contact=delivery_contact, - service_info=service_info, - shipment_notific_email=shipment_notific_email, - tracking_notific_email=tracking_notific_email, - ) - - if service_info['service_provider'] == PACKLINK_PROVIDER: - shipment_info = create_packlink_shipment( - pickup_address=pickup_address, - delivery_address=delivery_address, - shipment_parcel=shipment_parcel, - description_of_content=description_of_content, - pickup_date=pickup_date, - value_of_goods=value_of_goods, - pickup_contact=pickup_contact, - delivery_contact=delivery_contact, - service_info=service_info, - ) - - if service_info['service_provider'] == SENDCLOUD_PROVIDER: - shipment_info = create_sendcloud_shipment( - shipment=shipment, - delivery_address=delivery_address, - shipment_parcel=shipment_parcel, - description_of_content=description_of_content, - value_of_goods=value_of_goods, - delivery_contact=delivery_contact, - service_info=service_info, - ) - - if shipment_info: - fields = ['service_provider', 'carrier', 'carrier_service', 'shipment_id', 'shipment_amount', 'awb_number'] - for field in fields: - frappe.db.set_value('Shipment', shipment, field, shipment_info.get(field)) - frappe.db.set_value('Shipment', shipment, 'status', 'Booked') - if delivery_notes: - update_delivery_note(delivery_notes=delivery_notes, shipment_info=shipment_info) - return shipment_info - - -@frappe.whitelist() -def print_shipping_label(service_provider, shipment_id): - if service_provider == LETMESHIP_PROVIDER: - shipping_label = get_letmeship_label(shipment_id) - elif service_provider == PACKLINK_PROVIDER: - shipping_label = get_packlink_label(shipment_id) - elif service_provider == SENDCLOUD_PROVIDER: - shipping_label = get_sendcloud_label(shipment_id) - return shipping_label - - -@frappe.whitelist() -def update_tracking(shipment, service_provider, shipment_id, delivery_notes=[]): - # Update Tracking info in Shipment - tracking_data = None - if service_provider == LETMESHIP_PROVIDER: - tracking_data = get_letmeship_tracking_data(shipment_id) - elif service_provider == PACKLINK_PROVIDER: - tracking_data = get_packlink_tracking_data(shipment_id) - elif service_provider == SENDCLOUD_PROVIDER: - tracking_data = get_sendcloud_tracking_data(shipment_id) - if tracking_data: - if delivery_notes: - update_delivery_note(delivery_notes=delivery_notes, tracking_info=tracking_data) - frappe.db.set_value('Shipment', shipment, 'awb_number', tracking_data.get('awb_number')) - frappe.db.set_value('Shipment', shipment, 'tracking_status', tracking_data.get('tracking_status')) - frappe.db.set_value('Shipment', shipment, 'tracking_status_info', tracking_data.get('tracking_status_info')) - frappe.db.set_value('Shipment', shipment, 'tracking_url', tracking_data.get('tracking_url')) - @frappe.whitelist() def get_address_name(ref_doctype, docname): # Return address name @@ -199,90 +42,6 @@ def get_contact_name(ref_doctype, docname): # Return address name return get_default_contact(ref_doctype, docname) -def update_delivery_note(delivery_notes, shipment_info=None, tracking_info=None): - # Update Shipment Info in Delivery Note - # Using db_set since some services might not exist - for delivery_note in json.loads(delivery_notes): - dl_doc = frappe.get_doc('Delivery Note', delivery_note) - if shipment_info: - dl_doc.db_set('delivery_type', 'Parcel Service') - dl_doc.db_set('parcel_service', shipment_info.get('carrier')) - dl_doc.db_set('parcel_service_type', shipment_info.get('carrier_service')) - if tracking_info: - dl_doc.db_set('tracking_number', tracking_info.get('awb_number')) - dl_doc.db_set('tracking_url', tracking_info.get('tracking_url')) - dl_doc.db_set('tracking_status', tracking_info.get('tracking_status')) - dl_doc.db_set('tracking_status_info', tracking_info.get('tracking_status_info')) - - -def update_tracking_info(): - # Daily scheduled event to update Tracking info for not delivered Shipments - # Also Updates the related Delivery Notes - shipments = frappe.get_all('Shipment', filters={ - 'docstatus': 1, - 'status': 'Booked', - 'shipment_id': ['!=', ''], - 'tracking_status': ['!=', 'Delivered'], - }) - for shipment in shipments: - shipment_doc = frappe.get_doc('Shipment', shipment.name) - tracking_info = \ - update_tracking( - shipment_doc.service_provider, - shipment_doc.shipment_id, - shipment_doc.shipment_delivery_notes - ) - if tracking_info: - shipment_doc.db_set('awb_number', tracking_info.get('awb_number')) - shipment_doc.db_set('tracking_url', tracking_info.get('tracking_url')) - shipment_doc.db_set('tracking_status', tracking_info.get('tracking_status')) - shipment_doc.db_set('tracking_status_info', tracking_info.get('tracking_status_info')) - - -def get_address(address_name): - address = frappe.db.get_value('Address', address_name, [ - 'address_title', - 'address_line1', - 'address_line2', - 'city', - 'pincode', - 'country', - ], as_dict=1) - address.country_code = frappe.db.get_value('Country', address.country, 'code').upper() - if not address.pincode or address.pincode == '': - frappe.throw(_("Postal Code is mandatory to continue.
\ - Please set Postal Code for Address {1}" - ).format(address_name, address_name)) - address.pincode = address.pincode.replace(' ', '') - address.city = address.city.strip() - return address - - -def get_contact(contact_name): - contact = frappe.db.get_value('Contact', contact_name, [ - 'first_name', - 'last_name', - 'email_id', - 'phone', - 'mobile_no', - 'gender', - ], as_dict=1) - if not contact.last_name: - frappe.throw(_("Last Name is mandatory to continue.
\ - Please set Last Name for Contact {1}" - ).format(contact_name, contact_name)) - if not contact.phone: - contact.phone = contact.mobile_no - return contact - -def match_parcel_service_type_carrier(shipment_prices, reference): - for idx, prices in enumerate(shipment_prices): - service_name = match_parcel_service_type_alias(prices.get(reference[0]), prices.get(reference[1])) - is_preferred = frappe.db.get_value('Parcel Service Type', service_name, 'show_in_preferred_services_list') - shipment_prices[idx].service_name = service_name - shipment_prices[idx].is_preferred = is_preferred - return shipment_prices - @frappe.whitelist() def get_company_contact(user): contact = frappe.db.get_value('User', user, [ @@ -295,4 +54,4 @@ def get_company_contact(user): ], as_dict=1) if not contact.phone: contact.phone = contact.mobile_no - return contact \ No newline at end of file + return contact diff --git a/erpnext/stock/doctype/shipment/shipment_service_selector.html b/erpnext/stock/doctype/shipment/shipment_service_selector.html deleted file mode 100644 index 4ccbe34e9e..0000000000 --- a/erpnext/stock/doctype/shipment/shipment_service_selector.html +++ /dev/null @@ -1,74 +0,0 @@ -{% if (data.preferred_services.length || data.other_services.length) { %} -
-
{{ __("Preferred Services") }}
- {% if (data.preferred_services.length) { %} - - - - {% for (var i = 0; i < header_columns.length; i++) { %} - - {% } %} - - - - {% for (var i = 0; i < data.preferred_services.length; i++) { %} - - - - - - - - {% } %} - -
{{ header_columns[i] }}
{{ data.preferred_services[i].service_provider }}{{ data.preferred_services[i].carrier }}{{ data.preferred_services[i].service_name }}{{ format_currency(data.preferred_services[i].total_price, 'EUR', 2) }} - -
- {% } else { %} -
{{ __("No Preferred Services Available") }}
- {% } %} -
{{ __("Other Services") }}
- {% if (data.other_services.length) { %} - - - - {% for (var i = 0; i < header_columns.length; i++) { %} - - {% } %} - - - - {% for (var i = 0; i < data.other_services.length; i++) { %} - - - - - - - - {% } %} - -
{{ header_columns[i] }}
{{ data.other_services[i].service_provider }}{{ data.other_services[i].carrier }}{{ data.other_services[i].service_name }}{{ format_currency(data.other_services[i].total_price, 'EUR', 2) }} - -
- {% } else { %} -
{{ __("No Services Available") }}
- {% } %} -
-{% } else { %} -
{{ __("No Services Available") }}
-{% } %} - - \ No newline at end of file From 28055f483dff5c31c3babff2f21902c77ea5f6d7 Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Mon, 23 Nov 2020 06:12:50 +0000 Subject: [PATCH 218/229] fix(shipment): change shipment test and refactor shipment.js --- .../doctype/delivery_note/delivery_note.py | 1 + erpnext/stock/doctype/shipment/shipment.js | 101 +++--------------- erpnext/stock/doctype/shipment/shipment.json | 18 +++- .../stock/doctype/shipment/test_shipment.py | 101 ++---------------- 4 files changed, 38 insertions(+), 183 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 26e4f1633e..979e83df69 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -619,6 +619,7 @@ def make_shipment(source_name, target_doc=None): "name": "prevdoc_detail_docname", "parent": "prevdoc_docname", "parenttype": "prevdoc_doctype", + "base_amount": "grand_total" } } }, target_doc, postprocess) diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index aa792a4883..62070e4e55 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -23,76 +23,20 @@ frappe.ui.form.on('Shipment', { }, onload: function(frm) { frm.set_query("delivery_address_name", () => { - let link_doctype = ''; - let link_name = ''; - let is_your_company_address = 0; - if (frm.doc.delivery_to_type == 'Customer') { - link_doctype = 'Customer'; - link_name = frm.doc.delivery_customer; - } - if (frm.doc.delivery_to_type == 'Supplier') { - link_doctype = 'Supplier'; - link_name = frm.doc.delivery_supplier; - } - if (frm.doc.delivery_to_type == 'Company') { - link_doctype = 'Company'; - link_name = frm.doc.delivery_company; - is_your_company_address = 1; - } - return frm.events.address_query(frm, link_doctype, link_name, is_your_company_address); + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}` + return frm.events.address_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to], frm.doc.delivery_to_type === 'Company' ? 1 : 0); }); frm.set_query("pickup_address_name", () => { - let link_doctype = ''; - let link_name = ''; - let is_your_company_address = 0; - if (frm.doc.pickup_from_type == 'Customer') { - link_doctype = 'Customer'; - link_name = frm.doc.pickup_customer; - } - if (frm.doc.pickup_from_type == 'Supplier') { - link_doctype = 'Supplier'; - link_name = frm.doc.pickup_supplier; - } - if (frm.doc.pickup_from_type == 'Company') { - link_doctype = 'Company'; - link_name = frm.doc.pickup_company; - is_your_company_address = 1; - } - return frm.events.address_query(frm, link_doctype, link_name, is_your_company_address); + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}` + return frm.events.address_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from], frm.doc.pickup_from_type === 'Company' ? 1 : 0); }); frm.set_query("delivery_contact_name", () => { - let link_doctype = ''; - let link_name = ''; - if (frm.doc.delivery_to_type == 'Customer') { - link_doctype = 'Customer'; - link_name = frm.doc.delivery_customer; - } - if (frm.doc.delivery_to_type == 'Supplier') { - link_doctype = 'Supplier'; - link_name = frm.doc.delivery_supplier; - } - if (frm.doc.delivery_to_type == 'Company') { - link_doctype = 'Company'; - link_name = frm.doc.delivery_company; - } - return frm.events.contact_query(frm, link_doctype, link_name); + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}` + return frm.events.contact_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to]); }); frm.set_query("pickup_contact_name", () => { - let link_doctype = ''; - let link_name = ''; - if (frm.doc.pickup_from_type == 'Customer') { - link_doctype = 'Customer'; - link_name = frm.doc.pickup_customer; - } - if (frm.doc.pickup_from_type == 'Supplier') { - link_doctype = 'Supplier'; - link_name = frm.doc.pickup_supplier; - } - if (frm.doc.pickup_from_type == 'Company') { - link_doctype = 'Company'; - link_name = frm.doc.pickup_company; - } - return frm.events.contact_query(frm, link_doctype, link_name); + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}` + return frm.events.contact_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from]); }); frm.set_query("delivery_note", "shipment_delivery_note", function() { let customer = ''; @@ -120,24 +64,10 @@ frappe.ui.form.on('Shipment', { $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); }, before_save: function(frm) { - if (frm.doc.delivery_to_type == 'Company') { - frm.set_value("delivery_to", frm.doc.delivery_company); - } - if (frm.doc.delivery_to_type == 'Customer') { - frm.set_value("delivery_to", frm.doc.delivery_customer); - } - if (frm.doc.delivery_to_type == 'Supplier') { - frm.set_value("delivery_to", frm.doc.delivery_supplier); - } - if (frm.doc.pickup_from_type == 'Company') { - frm.set_value("pickup", frm.doc.pickup_company); - } - if (frm.doc.pickup_from_type == 'Customer') { - frm.set_value("pickup", frm.doc.pickup_customer); - } - if (frm.doc.pickup_from_type == 'Supplier') { - frm.set_value("pickup", frm.doc.pickup_supplier); - } + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}` + frm.set_value("delivery_to", frm.doc[delivery_to]); + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}` + frm.set_value("pickup", frm.doc[pickup_from]); }, set_pickup_company_address: function(frm) { frappe.db.get_value('Address', { @@ -476,18 +406,11 @@ frappe.ui.form.on('Shipment', { current_min = '00'; current_hour = Number(current_hour)+1; } - if (Number(current_hour) > 19 || Number(current_hour) === 19){ - frappe.throw(__("Today's pickup time is over, please select different date")); - } - current_hour = (current_hour < 10) ? '0' + current_hour : current_hour; let pickup_time = current_hour +':'+ current_min; return pickup_time; }, set_pickup_to_time: function(frm) { let pickup_to_hour = Number(frm.doc.pickup_from.split(':')[0])+5; - if (Number(pickup_to_hour) > 19 || Number(pickup_to_hour) === 19){ - pickup_to_hour = 19; - } let pickup_to_min = frm.doc.pickup_from.split(':')[1]; let pickup_to = pickup_to_hour +':'+ pickup_to_min; frm.set_value("pickup_to", pickup_to); diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index 9ac6102ded..7e2c5baace 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -460,13 +460,28 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-20 16:19:06.157106", + "modified": "2020-11-23 16:26:28.132608", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", "owner": "Administrator", "permissions": [ { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -476,6 +491,7 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index 6a06930e82..f61b87fd41 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -7,89 +7,19 @@ from datetime import date, timedelta import frappe import unittest -from erpnext.stock.doctype.shipment.shipment import fetch_shipping_rates -from erpnext.stock.doctype.shipment.shipment import create_shipment -from erpnext.stock.doctype.shipment.shipment import update_tracking +from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment class TestShipment(unittest.TestCase): - pass - - def test_shipment_booking(self): - shipment = create_test_shipment() - try: - shipment.submit() - except: - frappe.throw('Error occurred on submit shipment') - doc, rate, tracking_data = make_shipment_transaction(shipment) - if doc and rate and tracking_data: - self.assertEqual(doc.service_provider, rate.get('service_provider')) - self.assertEqual(doc.shipment_amount, rate.get('actual_price')) - self.assertEqual(doc.carrier, rate.get('carrier')) - self.assertEqual(doc.tracking_status, tracking_data.get('tracking_status')) - self.assertEqual(doc.tracking_url, tracking_data.get('tracking_url')) - def test_shipment_from_delivery_note(self): delivery_note = create_test_delivery_note() - try: - delivery_note.submit() - except: - frappe.throw('An error occurred.') - + delivery_note.submit() shipment = create_test_shipment([ delivery_note ]) - try: - shipment.submit() - except: - frappe.throw('Error occurred on submit shipment') - doc, rate, tracking_data = make_shipment_transaction(shipment) - if doc and rate and tracking_data: - self.assertEqual(doc.service_provider, rate.get('service_provider')) - self.assertEqual(doc.shipment_amount, rate.get('actual_price')) - self.assertEqual(doc.carrier, rate.get('carrier')) - self.assertEqual(doc.tracking_status, tracking_data.get('tracking_status')) - self.assertEqual(doc.tracking_url, tracking_data.get('tracking_url')) - - - -def make_shipment_transaction(shipment): - shipment_parcel = convert_shipmet_parcel(shipment.shipment_parcel) - shipment_rates = fetch_shipping_rates(shipment.pickup_from_type, shipment.delivery_to_type, - shipment.pickup_address_name, shipment.delivery_address_name, - shipment_parcel, shipment.description_of_content, - shipment.pickup_date, shipment.value_of_goods, - pickup_contact_name=shipment.pickup_contact_name, - delivery_contact_name=shipment.delivery_contact_name - ) - if len(shipment_rates) > 0: - # We are taking the first shipment rate - rate = shipment_rates[0] - new_shipment = create_shipment( - shipment=shipment.name, - pickup_from_type=shipment.pickup_from_type, - delivery_to_type=shipment.delivery_to_type, - pickup_address_name=shipment.pickup_address_name, - delivery_address_name=shipment.delivery_address_name, - shipment_parcel=shipment_parcel, - description_of_content=shipment.description_of_content, - pickup_date=shipment.pickup_date, - pickup_contact_name=shipment.pickup_contact_name, - delivery_contact_name=shipment.delivery_contact_name, - value_of_goods=shipment.value_of_goods, - service_data=json.dumps(rate), - shipment_notific_email=None, - tracking_notific_email=None, - delivery_notes=None - ) - service_provider = rate.get('service_provider') - shipment_id = new_shipment.get('shipment_id') - tracking_data = update_tracking( - shipment.name, - service_provider, - shipment_id, - delivery_notes=None - ) - doc = frappe.get_doc('Shipment', shipment.name) - return doc, rate, tracking_data - return None, None, None + shipment.submit() + second_shipment = make_shipment(delivery_note.name) + self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) + self.assertEqual(second_shipment.grand_total, delivery_note.grand_total) + self.assertEqual(len(second_shipment.shipment_delivery_note), 1) + self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) def create_test_delivery_note(): company = get_shipment_company() @@ -316,18 +246,3 @@ def create_shipment_item(item_name, company_name): except: frappe.throw('An error occurred.') return item - - -def convert_shipmet_parcel(shipmet_parcel): - data = [] - for parcel in shipmet_parcel: - data.append( - { - "length": parcel.length, - "width": parcel.width, - "height": parcel.height, - "weight": parcel.weight, - "count": parcel.count - } - ) - return json.dumps(data) From 99361b4a9eb88b56b90474fe42ccd2ba4f081afb Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Mon, 23 Nov 2020 09:03:13 +0000 Subject: [PATCH 219/229] chore: remove notification details section --- erpnext/stock/doctype/shipment/shipment.js | 72 -------------------- erpnext/stock/doctype/shipment/shipment.json | 56 +-------------- 2 files changed, 1 insertion(+), 127 deletions(-) diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index 62070e4e55..2832c8c72a 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -102,8 +102,6 @@ frappe.ui.form.on('Shipment', { frm.set_value("pickup_customer", ''); frm.set_value("pickup_company", ''); } - frm.events.remove_notific_child_table(frm, 'shipment_notification_subscription', 'Pickup'); - frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscription', 'Pickup'); }, delivery_to_type: function(frm) { if (frm.doc.delivery_to_type == 'Company') { @@ -126,8 +124,6 @@ frappe.ui.form.on('Shipment', { else { frm.toggle_display("shipment_delivery_note", true); } - frm.events.remove_notific_child_table(frm, 'shipment_notification_subscription', 'Delivery'); - frm.events.remove_notific_child_table(frm, 'shipment_status_update_subscription', 'Delivery'); }, delivery_address_name: function(frm) { if (frm.doc.delivery_to_type == 'Company') { @@ -427,80 +423,12 @@ frappe.ui.form.on('Shipment', { frm.set_value(field, ''); } }, - pickup_from_send_shipping_notification: function(frm, cdt, cdn) { - if (frm.doc.pickup_contact_email && frm.doc.pickup_from_send_shipping_notification - && !validate_duplicate(frm, 'shipment_notification_subscription', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { - let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscription", "shipment_notification_subscription"); - row.email = frm.doc.pickup_contact_email; - frm.refresh_fields("shipment_notification_subscription"); - } - if (!frm.doc.pickup_from_send_shipping_notification) { - frm.events.remove_email_row(frm, 'shipment_notification_subscription', frm.doc.pickup_contact_email); - frm.refresh_fields("shipment_notification_subscription"); - } - }, - pickup_from_subscribe_to_status_updates: function(frm, cdt, cdn) { - if (frm.doc.pickup_contact_email && frm.doc.pickup_from_subscribe_to_status_updates - && !validate_duplicate(frm, 'shipment_status_update_subscription', frm.doc.pickup_contact_email, locals[cdt][cdn].idx)) { - let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscription", "shipment_status_update_subscription"); - row.email = frm.doc.pickup_contact_email; - frm.refresh_fields("shipment_status_update_subscription"); - } - if (!frm.doc.pickup_from_subscribe_to_status_updates) { - frm.events.remove_email_row(frm, 'shipment_status_update_subscription', frm.doc.pickup_contact_email); - frm.refresh_fields("shipment_status_update_subscription"); - } - }, - delivery_to_send_shipping_notification: function(frm, cdt, cdn) { - if (frm.doc.delivery_contact_email && frm.doc.delivery_to_send_shipping_notification - && !validate_duplicate(frm, 'shipment_notification_subscription', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)){ - let row = frappe.model.add_child(frm.doc, "Shipment Notification Subscription", "shipment_notification_subscription"); - row.email = frm.doc.delivery_contact_email; - frm.refresh_fields("shipment_notification_subscription"); - } - if (!frm.doc.delivery_to_send_shipping_notification) { - frm.events.remove_email_row(frm, 'shipment_notification_subscription', frm.doc.delivery_contact_email); - frm.refresh_fields("shipment_notification_subscription"); - } - }, - delivery_to_subscribe_to_status_updates: function(frm, cdt, cdn) { - if (frm.doc.delivery_contact_email && frm.doc.delivery_to_subscribe_to_status_updates - && !validate_duplicate(frm, 'shipment_status_update_subscription', frm.doc.delivery_contact_email, locals[cdt][cdn].idx)) { - let row = frappe.model.add_child(frm.doc, "Shipment Status Update Subscription", "shipment_status_update_subscription"); - row.email = frm.doc.delivery_contact_email; - frm.refresh_fields("shipment_status_update_subscription"); - } - if (!frm.doc.delivery_to_subscribe_to_status_updates) { - frm.events.remove_email_row(frm, 'shipment_status_update_subscription', frm.doc.delivery_contact_email); - frm.refresh_fields("shipment_status_update_subscription"); - } - }, remove_email_row: function(frm, table, fieldname) { $.each(frm.doc[table] || [], function(i, detail) { if(detail.email === fieldname){ cur_frm.get_field(table).grid.grid_rows[i].remove(); } }); - }, - remove_notific_child_table: function(frm, table, delivery_type) { - $.each(frm.doc[table] || [], function(i, detail) { - if (detail.email != frm.doc.pickup_email || detail.email != frm.doc.delivery_email){ - cur_frm.get_field(table).grid.grid_rows[i].remove(); - } - }); - frm.refresh_fields(table); - if (delivery_type == 'Delivery') { - frm.set_value("delivery_to_send_shipping_notification", 0); - frm.set_value("delivery_to_subscribe_to_status_updates", 0); - frm.refresh_fields("delivery_to_send_shipping_notification"); - frm.refresh_fields("delivery_to_subscribe_to_status_updates"); - } - else { - frm.set_value("pickup_from_send_shipping_notification", 0); - frm.set_value("pickup_from_subscribe_to_status_updates", 0); - frm.refresh_fields("pickup_from_send_shipping_notification"); - frm.refresh_fields("pickup_from_subscribe_to_status_updates"); - } } }); diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index 7e2c5baace..1ae7862bc9 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -30,14 +30,6 @@ "delivery_contact_name", "delivery_contact_email", "delivery_contact", - "notification_details_section", - "pickup_from_send_shipping_notification", - "pickup_from_subscribe_to_status_updates", - "shipment_notification_subscription", - "column_break_27", - "delivery_to_send_shipping_notification", - "delivery_to_subscribe_to_status_updates", - "shipment_status_update_subscription", "parcels_section", "shipment_parcel", "parcel_template", @@ -224,40 +216,6 @@ "fieldtype": "Small Text", "read_only": 1 }, - { - "collapsible": 1, - "fieldname": "notification_details_section", - "fieldtype": "Section Break", - "label": "Notification Details" - }, - { - "default": "0", - "fieldname": "pickup_from_send_shipping_notification", - "fieldtype": "Check", - "label": "Send shipping notification" - }, - { - "default": "0", - "fieldname": "pickup_from_subscribe_to_status_updates", - "fieldtype": "Check", - "label": "Subscribe to status updates" - }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "delivery_to_send_shipping_notification", - "fieldtype": "Check", - "label": "Send shipping notification" - }, - { - "default": "0", - "fieldname": "delivery_to_subscribe_to_status_updates", - "fieldtype": "Check", - "label": "Subscribe to status updates" - }, { "fieldname": "parcels_section", "fieldtype": "Section Break", @@ -437,18 +395,6 @@ "label": "Shipment Delivery Note", "options": "Shipment Delivery Note" }, - { - "fieldname": "shipment_notification_subscription", - "fieldtype": "Table", - "label": "Shipment Notification Subscription", - "options": "Shipment Notification Subscription" - }, - { - "fieldname": "shipment_status_update_subscription", - "fieldtype": "Table", - "label": "Shipment Status Update Subscription", - "options": "Shipment Status Update Subscription" - }, { "depends_on": "eval:doc.pickup_from_type === 'Company'", "fieldname": "pickup_contact_person", @@ -460,7 +406,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-23 16:26:28.132608", + "modified": "2020-11-23 17:00:51.600965", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", From b4b542d1c392b865c96a5b82aa3927fb95aca60d Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Mon, 23 Nov 2020 09:26:39 +0000 Subject: [PATCH 220/229] chore: linter issues and sider checks --- erpnext/stock/doctype/shipment/shipment.js | 92 ++++++++----------- erpnext/stock/doctype/shipment/shipment.py | 1 - .../stock/doctype/shipment/shipment_list.js | 2 +- .../stock/doctype/shipment/test_shipment.py | 11 +-- 4 files changed, 42 insertions(+), 64 deletions(-) diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js index 2832c8c72a..5ccb7d2ff6 100644 --- a/erpnext/stock/doctype/shipment/shipment.js +++ b/erpnext/stock/doctype/shipment/shipment.js @@ -23,19 +23,19 @@ frappe.ui.form.on('Shipment', { }, onload: function(frm) { frm.set_query("delivery_address_name", () => { - let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}` + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; return frm.events.address_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to], frm.doc.delivery_to_type === 'Company' ? 1 : 0); }); frm.set_query("pickup_address_name", () => { - let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}` + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; return frm.events.address_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from], frm.doc.pickup_from_type === 'Company' ? 1 : 0); }); frm.set_query("delivery_contact_name", () => { - let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}` + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; return frm.events.contact_query(frm, frm.doc.delivery_to_type, frm.doc[delivery_to]); }); frm.set_query("pickup_contact_name", () => { - let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}` + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; return frm.events.contact_query(frm, frm.doc.pickup_from_type, frm.doc[pickup_from]); }); frm.set_query("delivery_note", "shipment_delivery_note", function() { @@ -57,16 +57,16 @@ frappe.ui.form.on('Shipment', { } }); }, - refresh: function(frm) { + refresh: function() { $('div[data-fieldname=pickup_address] > div > .clearfix').hide(); $('div[data-fieldname=pickup_contact] > div > .clearfix').hide(); $('div[data-fieldname=delivery_address] > div > .clearfix').hide(); $('div[data-fieldname=delivery_contact] > div > .clearfix').hide(); }, before_save: function(frm) { - let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}` + let delivery_to = `delivery_${frappe.model.scrub(frm.doc.delivery_to_type)}`; frm.set_value("delivery_to", frm.doc[delivery_to]); - let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}` + let pickup_from = `pickup_${frappe.model.scrub(frm.doc.pickup_from_type)}`; frm.set_value("pickup", frm.doc[pickup_from]); }, set_pickup_company_address: function(frm) { @@ -90,8 +90,7 @@ frappe.ui.form.on('Shipment', { frm.set_value("pickup_company", frappe.defaults.get_default('company')); frm.set_value("pickup_customer", ''); frm.set_value("pickup_supplier", ''); - } - else { + } else { frm.trigger('clear_pickup_fields'); } if (frm.doc.pickup_from_type == 'Customer') { @@ -108,8 +107,7 @@ frappe.ui.form.on('Shipment', { frm.set_value("delivery_company", frappe.defaults.get_default('company')); frm.set_value("delivery_customer", ''); frm.set_value("delivery_supplier", ''); - } - else { + } else { frm.trigger('clear_delivery_fields'); } if (frm.doc.delivery_to_type == 'Customer') { @@ -120,24 +118,21 @@ frappe.ui.form.on('Shipment', { frm.set_value("delivery_customer", ''); frm.set_value("delivery_company", ''); frm.toggle_display("shipment_delivery_note", false); - } - else { + } else { frm.toggle_display("shipment_delivery_note", true); } }, delivery_address_name: function(frm) { if (frm.doc.delivery_to_type == 'Company') { erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', true); - } - else { + } else { erpnext.utils.get_address_display(frm, 'delivery_address_name', 'delivery_address', false); } }, pickup_address_name: function(frm) { if (frm.doc.pickup_from_type == 'Company') { erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', true); - } - else { + } else { erpnext.utils.get_address_display(frm, 'pickup_address_name', 'pickup_address', false); } }, @@ -146,18 +141,16 @@ frappe.ui.form.on('Shipment', { method: "frappe.contacts.doctype.contact.contact.get_contact_details", args: { contact: contact_name }, callback: function(r) { - if(r.message) { + if (r.message) { if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) { if (contact_type == 'Delivery') { frm.set_value('delivery_contact_name', ''); frm.set_value('delivery_contact', ''); - } - else { + } else { frm.set_value('pickup_contact_name', ''); frm.set_value('pickup_contact', ''); } - frappe.throw(__(`Email or Phone/Mobile of the Contact are mandatory to continue.
- Please set Email/Phone for the contact ${contact_name}`)); + frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") + "
" + __("Please set Email/Phone for the contact") + ` ${contact_name}`); } let contact_display = r.message.contact_display; if (r.message.contact_email) { @@ -169,13 +162,12 @@ frappe.ui.form.on('Shipment', { if (r.message.contact_mobile && !r.message.contact_phone) { contact_display += '
' + r.message.contact_mobile; } - if (contact_type == 'Delivery'){ + if (contact_type == 'Delivery') { frm.set_value('delivery_contact', contact_display); if (r.message.contact_email) { frm.set_value('delivery_contact_email', r.message.contact_email); } - } - else { + } else { frm.set_value('pickup_contact', contact_display); if (r.message.contact_email) { frm.set_value('pickup_contact_email', r.message.contact_email); @@ -246,13 +238,11 @@ frappe.ui.form.on('Shipment', { if (delivery_type == 'Delivery') { frm.set_value('delivery_company', ''); frm.set_value('delivery_contact', ''); - } - else { + } else { frm.set_value('pickup_company', ''); frm.set_value('pickup_contact', ''); } - frappe.throw(__(`Last Name, Email or Phone/Mobile of the user are mandatory to continue.
- Please first set Last Name, Email and Phone for the user ${frappe.session.user}`)); + frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "
" + __("Please first set Last Name, Email and Phone for the user") + ` ${frappe.session.user}`); } let contact_display = r.full_name; if (r.email) { @@ -269,8 +259,7 @@ frappe.ui.form.on('Shipment', { if (r.email) { frm.set_value('delivery_contact_email', r.email); } - } - else { + } else { frm.set_value('pickup_contact', contact_display); if (r.email) { frm.set_value('pickup_contact_email', r.email); @@ -294,27 +283,27 @@ frappe.ui.form.on('Shipment', { delivery_customer: function(frm) { frm.trigger('clear_delivery_fields'); if (frm.doc.delivery_customer) { - frm.events.set_address_name(frm,'Customer',frm.doc.delivery_customer, 'Delivery'); - frm.events.set_contact_name(frm,'Customer',frm.doc.delivery_customer, 'Delivery'); + frm.events.set_address_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); + frm.events.set_contact_name(frm, 'Customer', frm.doc.delivery_customer, 'Delivery'); } }, delivery_supplier: function(frm) { frm.trigger('clear_delivery_fields'); if (frm.doc.delivery_supplier) { - frm.events.set_address_name(frm,'Supplier',frm.doc.delivery_supplier, 'Delivery'); - frm.events.set_contact_name(frm,'Supplier',frm.doc.delivery_supplier, 'Delivery'); + frm.events.set_address_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); + frm.events.set_contact_name(frm, 'Supplier', frm.doc.delivery_supplier, 'Delivery'); } }, pickup_customer: function(frm) { if (frm.doc.pickup_customer) { - frm.events.set_address_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup'); - frm.events.set_contact_name(frm,'Customer',frm.doc.pickup_customer, 'Pickup'); + frm.events.set_address_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); + frm.events.set_contact_name(frm, 'Customer', frm.doc.pickup_customer, 'Pickup'); } }, pickup_supplier: function(frm) { if (frm.doc.pickup_supplier) { - frm.events.set_address_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup'); - frm.events.set_contact_name(frm,'Supplier',frm.doc.pickup_supplier, 'Pickup'); + frm.events.set_address_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); + frm.events.set_contact_name(frm, 'Supplier', frm.doc.pickup_supplier, 'Pickup'); } }, set_address_name: function(frm, ref_doctype, ref_docname, delivery_type) { @@ -325,11 +314,10 @@ frappe.ui.form.on('Shipment', { docname: ref_docname }, callback: function(r) { - if(r.message) { + if (r.message) { if (delivery_type == 'Delivery') { frm.set_value('delivery_address_name', r.message); - } - else { + } else { frm.set_value('pickup_address_name', r.message); } } @@ -344,11 +332,10 @@ frappe.ui.form.on('Shipment', { docname: ref_docname }, callback: function(r) { - if(r.message) { + if (r.message) { if (delivery_type == 'Delivery') { frm.set_value('delivery_contact_name', r.message); - } - else { + } else { frm.set_value('pickup_contact_name', r.message); } } @@ -397,8 +384,7 @@ frappe.ui.form.on('Shipment', { let current_min = new Date().toLocaleString('en-US', {minute: 'numeric'}); if (current_min < 30) { current_min = '30'; - } - else { + } else { current_min = '00'; current_hour = Number(current_hour)+1; } @@ -413,19 +399,19 @@ frappe.ui.form.on('Shipment', { }, clear_pickup_fields: function(frm) { let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"]; - for (let field of fields){ + for (let field of fields) { frm.set_value(field, ''); } }, clear_delivery_fields: function(frm) { let fields = ["delivery_address_name", "delivery_contact_name", "delivery_address", "delivery_contact", "delivery_contact_email"]; - for (let field of fields){ + for (let field of fields) { frm.set_value(field, ''); } }, remove_email_row: function(frm, table, fieldname) { $.each(frm.doc[table] || [], function(i, detail) { - if(detail.email === fieldname){ + if (detail.email === fieldname) { cur_frm.get_field(table).grid.grid_rows[i].remove(); } }); @@ -437,8 +423,8 @@ frappe.ui.form.on('Shipment Delivery Note', { let row = locals[cdt][cdn]; if (row.delivery_note) { let row_index = row.idx - 1; - if(validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) { - frappe.throw(__(`You have entered a duplicate Delivery Note on Row ${row.idx}. Please rectify and try again.`)); + if (validate_duplicate(frm, 'shipment_delivery_note', row.delivery_note, row_index)) { + frappe.throw(__("You have entered a duplicate Delivery Note on Row") + ` ${row.idx}. ` + __("Please rectify and try again.")); } } }, @@ -452,7 +438,7 @@ frappe.ui.form.on('Shipment Delivery Note', { }, }); -var validate_duplicate = function(frm, table, fieldname, index){ +var validate_duplicate = function(frm, table, fieldname, index) { return ( table === 'shipment_delivery_note' ? frm.doc[table].some((detail, i) => detail.delivery_note === fieldname && !(index === i)) diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 4e16f95533..508af39cd5 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import frappe -import json from frappe import _ from frappe.utils import flt from frappe.model.document import Document diff --git a/erpnext/stock/doctype/shipment/shipment_list.js b/erpnext/stock/doctype/shipment/shipment_list.js index 57e92099cb..52b052c81f 100644 --- a/erpnext/stock/doctype/shipment/shipment_list.js +++ b/erpnext/stock/doctype/shipment/shipment_list.js @@ -1,7 +1,7 @@ frappe.listview_settings['Shipment'] = { add_fields: ["status"], get_indicator: function(doc) { - if(doc.status=='Booked') { + if (doc.status=='Booked') { return [__("Booked"), "green"]; } } diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index f61b87fd41..e238e878db 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -2,7 +2,6 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals -import json from datetime import date, timedelta import frappe @@ -222,10 +221,7 @@ def create_material_receipt(item, company): } ) stock.insert() - try: - stock.submit() - except: - frappe.throw('An error occurred.') + stock.submit() def create_shipment_item(item_name, company_name): @@ -241,8 +237,5 @@ def create_shipment_item(item_name, company_name): "default_warehouse": 'Stores - SC' } ) - try: - item.insert() - except: - frappe.throw('An error occurred.') + item.insert() return item From 8e68f128c17e5f14096edf6fbc0a12f951d63393 Mon Sep 17 00:00:00 2001 From: jbienesdev Date: Wed, 2 Dec 2020 07:54:25 +0000 Subject: [PATCH 221/229] fix: travis build error - Removed shipment notification and subscription files - Minor changes on shipment field configuration - Add shipment to desk --- erpnext/stock/desk_page/stock/stock.json | 4 +- erpnext/stock/doctype/shipment/shipment.json | 42 +++++++++++++++---- erpnext/stock/doctype/shipment/shipment.py | 7 ++++ .../stock/doctype/shipment/test_shipment.py | 7 ++-- .../shipment_delivery_note.json | 3 +- .../__init__.py | 0 .../shipment_notification_subscription.json | 40 ------------------ .../shipment_notification_subscription.py | 10 ----- .../__init__.py | 0 .../shipment_status_update_subscription.json | 40 ------------------ .../shipment_status_update_subscription.py | 10 ----- 11 files changed, 46 insertions(+), 117 deletions(-) delete mode 100644 erpnext/stock/doctype/shipment_notification_subscription/__init__.py delete mode 100644 erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json delete mode 100644 erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py delete mode 100644 erpnext/stock/doctype/shipment_status_update_subscription/__init__.py delete mode 100644 erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json delete mode 100644 erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 390fcd91e3..9068e338c3 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-10-07 18:40:17.130207", + "modified": "2020-12-02 15:47:41.532942", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index 1ae7862bc9..37a9cc6c02 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -234,6 +234,7 @@ "options": "Shipment Parcel Template" }, { + "depends_on": "eval:doc.docstatus !== 1\n", "fieldname": "add_template", "fieldtype": "Button", "label": "Add Template" @@ -262,6 +263,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "pickup_date", "fieldtype": "Date", "in_list_view": 1, @@ -269,12 +271,14 @@ "reqd": 1 }, { + "allow_on_submit": 1, "default": "09:00", "fieldname": "pickup_from", "fieldtype": "Time", "label": "Pickup from" }, { + "allow_on_submit": 1, "default": "17:00", "fieldname": "pickup_to", "fieldtype": "Time", @@ -316,57 +320,77 @@ { "fieldname": "service_provider", "fieldtype": "Data", - "label": "Service Provider" + "label": "Service Provider", + "no_copy": 1, + "print_hide": 1 }, { "fieldname": "shipment_id", "fieldtype": "Data", - "label": "Shipment ID" + "label": "Shipment ID", + "no_copy": 1, + "print_hide": 1 }, { "fieldname": "shipment_amount", "fieldtype": "Currency", "label": "Shipment Amount", - "precision": "2" + "no_copy": 1, + "precision": "2", + "print_hide": 1 }, { "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted" + "no_copy": 1, + "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", + "print_hide": 1 }, { "fieldname": "tracking_url", "fieldtype": "Small Text", "hidden": 1, "label": "Tracking URL", + "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { "fieldname": "carrier", "fieldtype": "Data", - "label": "Carrier" + "label": "Carrier", + "no_copy": 1, + "print_hide": 1 }, { "fieldname": "carrier_service", "fieldtype": "Data", - "label": "Carrier Service" + "label": "Carrier Service", + "no_copy": 1, + "print_hide": 1 }, { "fieldname": "awb_number", "fieldtype": "Data", - "label": "AWB Number" + "label": "AWB Number", + "no_copy": 1, + "print_hide": 1 }, { "fieldname": "tracking_status", "fieldtype": "Select", "label": "Tracking Status", - "options": "\nIn Progress\nDelivered\nReturned\nLost" + "no_copy": 1, + "options": "\nIn Progress\nDelivered\nReturned\nLost", + "print_hide": 1 }, { "fieldname": "tracking_status_info", "fieldtype": "Data", "label": "Tracking Status Info", + "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -406,7 +430,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-23 17:00:51.600965", + "modified": "2020-12-02 15:43:44.607039", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 508af39cd5..de0c243b05 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -13,6 +13,7 @@ from frappe.contacts.doctype.contact.contact import get_default_contact class Shipment(Document): def validate(self): self.validate_weight() + self.set_value_of_goods() if self.docstatus == 0: self.status = 'Draft' @@ -31,6 +32,12 @@ class Shipment(Document): if flt(parcel.weight) <= 0: frappe.throw(_('Parcel weight cannot be 0')) + def set_value_of_goods(self): + value_of_goods = 0 + for entry in self.get("shipment_delivery_note"): + value_of_goods += flt(entry.get("grand_total")) + self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods + @frappe.whitelist() def get_address_name(ref_doctype, docname): # Return address name diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index e238e878db..e1fa207a21 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -16,7 +16,6 @@ class TestShipment(unittest.TestCase): shipment.submit() second_shipment = make_shipment(delivery_note.name) self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) - self.assertEqual(second_shipment.grand_total, delivery_note.grand_total) self.assertEqual(len(second_shipment.shipment_delivery_note), 1) self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) @@ -49,7 +48,7 @@ def create_test_delivery_note(): return delivery_note -def create_test_shipment(delivery_notes=[]): +def create_test_shipment(delivery_notes = None): company = get_shipment_company() company_address = get_shipment_company_address(company.name) customer = get_shipment_customer() @@ -74,7 +73,7 @@ def create_test_shipment(delivery_notes=[]): shipment.pickup_to = '17:00' shipment.description_of_content = 'unit test entry' for delivery_note in delivery_notes: - shipment.append('shipment_delivery_notes', + shipment.append('shipment_delivery_note', { "delivery_note": delivery_note.name } @@ -229,7 +228,7 @@ def create_shipment_item(item_name, company_name): item.item_name = item_name item.item_code = item_name item.item_group = 'All Item Groups' - item.opening_stock = 'Nos' + item.stock_uom = 'Nos' item.standard_rate = 50 item.append('item_defaults', { diff --git a/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json index 9651e3f945..8625913718 100644 --- a/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json +++ b/erpnext/stock/doctype/shipment_delivery_note/shipment_delivery_note.json @@ -18,7 +18,6 @@ "reqd": 1 }, { - "fetch_from": "delivery_note.grand_total", "fieldname": "grand_total", "fieldtype": "Currency", "in_list_view": 1, @@ -28,7 +27,7 @@ ], "istable": 1, "links": [], - "modified": "2020-07-09 12:55:01.134270", + "modified": "2020-12-02 15:44:34.028703", "modified_by": "Administrator", "module": "Stock", "name": "Shipment Delivery Note", diff --git a/erpnext/stock/doctype/shipment_notification_subscription/__init__.py b/erpnext/stock/doctype/shipment_notification_subscription/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json b/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json deleted file mode 100644 index d927d9902e..0000000000 --- a/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "actions": [], - "creation": "2020-07-09 12:49:09.185552", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "email", - "unsubscribed" - ], - "fields": [ - { - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "email", - "reqd": 1, - "unique": 1 - }, - { - "default": "0", - "fieldname": "unsubscribed", - "fieldtype": "Check", - "in_list_view": 1, - "label": "unsubscribed" - } - ], - "istable": 1, - "links": [], - "modified": "2020-07-09 12:55:14.217387", - "modified_by": "Administrator", - "module": "Stock", - "name": "Shipment Notification Subscription", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py b/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py deleted file mode 100644 index c816e4343c..0000000000 --- a/erpnext/stock/doctype/shipment_notification_subscription/shipment_notification_subscription.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class ShipmentNotificationSubscription(Document): - pass diff --git a/erpnext/stock/doctype/shipment_status_update_subscription/__init__.py b/erpnext/stock/doctype/shipment_status_update_subscription/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json b/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json deleted file mode 100644 index a7fe4a4a0a..0000000000 --- a/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "actions": [], - "creation": "2020-07-09 12:51:10.656612", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "email", - "unsubscribed" - ], - "fields": [ - { - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "email", - "reqd": 1, - "unique": 1 - }, - { - "default": "0", - "fieldname": "unsubscribed", - "fieldtype": "Check", - "in_list_view": 1, - "label": "unsubscribed" - } - ], - "istable": 1, - "links": [], - "modified": "2020-07-09 12:55:27.615463", - "modified_by": "Administrator", - "module": "Stock", - "name": "Shipment Status Update Subscription", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py b/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py deleted file mode 100644 index 1b006d7efc..0000000000 --- a/erpnext/stock/doctype/shipment_status_update_subscription/shipment_status_update_subscription.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class ShipmentStatusUpdateSubscription(Document): - pass From 523c464a92bf5e17850b55da2d9980e7b00ea274 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Dec 2020 16:01:35 +0530 Subject: [PATCH 222/229] fix: Test Payment Based on Leave Application (Travis) --- erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 71cb4083ed..5daf1d439d 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -118,11 +118,6 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) - #Gross pay calculation based on attendances - gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) - - self.assertEqual(flt(ss.gross_pay, 2), flt(gross_pay, 2)) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") def test_salary_slip_with_holidays_included(self): From 8474476316095bc94286dd8dd8f6eb2f4decf651 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 24 Nov 2020 19:04:03 +0530 Subject: [PATCH 223/229] fix: incorrect balance value in stock balance report --- erpnext/stock/report/stock_balance/stock_balance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 1339d9b682..ccd01001bb 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -164,7 +164,7 @@ def get_stock_ledger_entries(filters, items): select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, - sle.item_code as name, sle.voucher_no + sle.item_code as name, sle.voucher_no, sle.stock_value from `tabStock Ledger Entry` sle force index (posting_sort_index) where sle.docstatus < 2 %s %s @@ -197,7 +197,7 @@ def get_item_warehouse_map(filters, sle): else: qty_diff = flt(d.actual_qty) - value_diff = flt(d.stock_value_difference) + value_diff = flt(d.stock_value) - flt(qty_dict.bal_val) if d.posting_date < from_date: qty_dict.opening_qty += qty_diff From 69c5d49b25002dffd8248df3f3916a9a3d1763ab Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 3 Dec 2020 15:14:54 +0530 Subject: [PATCH 224/229] refactor: to avoid using SQL query to get fiscal year filters --- .../accounts/doctype/fiscal_year/fiscal_year.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index d80bc7fad1..8faf24d375 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -116,12 +116,8 @@ def auto_create_fiscal_year(): pass def get_from_and_to_date(fiscal_year): - from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date - from `tabFiscal Year` where name=%s""", (fiscal_year))[0] - - from_and_to_date = { - "from_date": from_and_to_date_tuple[0], - "to_date": from_and_to_date_tuple[1] - } - - return from_and_to_date + fields = [ + "year_start_date as from_date", + "year_end_date as to_date" + ] + return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1) From 26cc662a7879ad1efdcd1d12e2a6c3ea27a93916 Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 3 Dec 2020 17:45:34 +0530 Subject: [PATCH 225/229] feat: added filter for customer field --- erpnext/support/doctype/issue/issue.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index fe01d4b983..33191bedde 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,6 +1,13 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; + frm.set_query('customer', function () { + return { + filters: { + "disabled": 0 + } + } + }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { @@ -145,6 +152,7 @@ frappe.ui.form.on("Issue", { reset_sla.show(); }, + timeline_refresh: function(frm) { // create button for "Help Article" if(frappe.model.can_create('Help Article')) { From 25e058833a7dfec25812d4fa5828d94ef61713a5 Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 3 Dec 2020 18:13:24 +0530 Subject: [PATCH 226/229] fix: sider changes --- erpnext/support/doctype/issue/issue.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 33191bedde..c20ad5c2fd 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -6,8 +6,8 @@ frappe.ui.form.on("Issue", { filters: { "disabled": 0 } - } - }); + }; + }); frappe.db.get_value("Support Settings", {name: "Support Settings"}, ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { From 0139109de2ae31b2283712ee5ea555ed62afae77 Mon Sep 17 00:00:00 2001 From: Anuja P Date: Fri, 4 Dec 2020 11:28:13 +0530 Subject: [PATCH 227/229] fix: consistency check --- erpnext/support/doctype/issue/issue.js | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index c20ad5c2fd..521e671b0d 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -1,7 +1,7 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; - frm.set_query('customer', function () { + frm.set_query("customer", function () { return { filters: { "disabled": 0 @@ -28,14 +28,14 @@ frappe.ui.form.on("Issue", { }, callback: function (r) { if (r && r.message) { - frm.set_query('priority', function() { + frm.set_query("priority", function() { return { filters: { "name": ["in", r.message.priority], } }; }); - frm.set_query('service_level_agreement', function() { + frm.set_query("service_level_agreement", function() { return { filters: { "name": ["in", r.message.service_level_agreements], @@ -52,9 +52,9 @@ frappe.ui.form.on("Issue", { if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { if (frm.doc.service_level_agreement) { frappe.call({ - 'method': 'frappe.client.get', + "method": "frappe.client.get", args: { - doctype: 'Service Level Agreement', + doctype: "Service Level Agreement", name: frm.doc.service_level_agreement }, callback: function(data) { @@ -134,8 +134,8 @@ frappe.ui.form.on("Issue", { reset_sla.clear(); frappe.show_alert({ - indicator: 'green', - message: __('Resetting Service Level Agreement.') + indicator: "green", + message: __("Resetting Service Level Agreement.") }); frm.call("reset_service_level_agreement", { @@ -155,33 +155,33 @@ frappe.ui.form.on("Issue", { timeline_refresh: function(frm) { // create button for "Help Article" - if(frappe.model.can_create('Help Article')) { + if(frappe.model.can_create("Help Article")) { // Removing Help Article button if exists to avoid multiple occurance frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove(); $('') .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])')) - .on('click', function() { - var content = $(this).parents('.timeline-item:first').find('.timeline-item-content').html(); - var doc = frappe.model.get_new_doc('Help Article'); + .on("click", function() { + var content = $(this).parents(".timeline-item:first").find(".timeline-item-content").html(); + var doc = frappe.model.get_new_doc("Help Article"); doc.title = frm.doc.subject; doc.content = content; - frappe.set_route('Form', 'Help Article', doc.name); + frappe.set_route("Form", "Help Article", doc.name); }); } - if (!frm.timeline.wrapper.find('.btn-split-issue').length) { + if (!frm.timeline.wrapper.find(".btn-split-issue").length) { let split_issue = __("Split Issue") $(``) .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])')) if (!frm.timeline.wrapper.data("split-issue-event-attached")){ - frm.timeline.wrapper.on('click', '.btn-split-issue', (e) => { + frm.timeline.wrapper.on("click", ".btn-split-issue", (e) => { var dialog = new frappe.ui.Dialog({ title: __("Split Issue"), fields: [ - {fieldname: 'subject', fieldtype: 'Data', reqd:1, label: __('Subject'), description: __('All communications including and above this shall be moved into the new Issue')} + {fieldname: "subject", fieldtype: "Data", reqd:1, label: __("Subject"), description: __("All communications including and above this shall be moved into the new Issue")} ], primary_action_label: __("Split"), primary_action: function() { @@ -234,7 +234,7 @@ function set_time_to_resolve_and_response(frm) { function get_time_left(timestamp, agreement_status) { const diff = moment(timestamp).diff(moment()); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; - let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green"; + let indicator = (diff_display == "Failed" && agreement_status != "Fulfilled") ? "red" : "green"; return {"diff_display": diff_display, "indicator": indicator}; } From 28e86cf18312155ed020065b10152f77e8747d8f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 4 Dec 2020 00:47:46 +0530 Subject: [PATCH 228/229] fix: pricing rule with transaction not working for additional product --- .../doctype/pricing_rule/pricing_rule.json | 4 ++- .../doctype/pricing_rule/test_pricing_rule.py | 31 +++++++++++++++---- .../accounts/doctype/pricing_rule/utils.py | 11 ++++++- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index cc8ed4bc49..d08a854142 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -406,6 +406,7 @@ "fieldtype": "Column Break" }, { + "default": "0", "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", @@ -469,6 +470,7 @@ "options": "UOM" }, { + "description": "If rate is zero them item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", "label": "Rate" @@ -563,7 +565,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2020-10-28 16:53:14.416172", + "modified": "2020-12-04 00:36:24.698219", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index ec0a485bfc..af8d21d9ce 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -521,6 +521,22 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() + def test_pricing_rule_for_transaction(self): + make_item("Water Flask 1") + frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product", + apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10) + + si = create_sales_invoice(qty=5, do_not_submit=True) + self.assertEquals(len(si.items), 2) + self.assertEquals(si.items[1].rate, 10) + + si1 = create_sales_invoice(qty=2, do_not_submit=True) + self.assertEquals(len(si1.items), 1) + + for doc in [si, si1]: + doc.delete() + def make_pricing_rule(**args): args = frappe._dict(args) @@ -539,20 +555,23 @@ def make_pricing_rule(**args): "rate_or_discount": args.rate_or_discount or "Discount Percentage", "discount_percentage": args.discount_percentage or 0.0, "rate": args.rate or 0.0, - "margin_type": args.margin_type, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "condition": args.condition or '', "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 }) - if args.get("priority"): - doc.priority = args.get("priority") + for field in ["free_item", "free_qty", "free_item_rate", "priority", + "margin_type", "price_or_product_discount"]: + if args.get(field): + doc.set(field, args.get(field)) apply_on = doc.apply_on.replace(' ', '_').lower() child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'} - doc.append(child_table.get(doc.apply_on), { - apply_on: args.get(apply_on) or "_Test Item" - }) + + if doc.apply_on != "Transaction": + doc.append(child_table.get(doc.apply_on), { + apply_on: args.get(apply_on) or "_Test Item" + }) doc.insert(ignore_permissions=True) if args.get(apply_on) and apply_on != "item_code": diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b003328cc4..2c7cd14451 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -457,6 +457,9 @@ def apply_pricing_rule_on_transaction(doc): pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules) + if not pricing_rules: + remove_free_item(doc) + for d in pricing_rules: if d.price_or_product_discount == 'Price': if d.apply_discount_on: @@ -480,6 +483,12 @@ def apply_pricing_rule_on_transaction(doc): get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() + doc.calculate_taxes_and_totals() + +def remove_free_item(doc): + for d in doc.items: + if d.is_free_item: + doc.remove(d) def get_applied_pricing_rules(pricing_rules): if pricing_rules: @@ -492,7 +501,7 @@ def get_applied_pricing_rules(pricing_rules): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item - if pricing_rule.same_item: + if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction': free_item = item_details.item_code or args.item_code if not free_item: From 931f2e73a7a741a695072fe598f284d4b22b435b Mon Sep 17 00:00:00 2001 From: Anuja P Date: Fri, 4 Dec 2020 14:57:23 +0530 Subject: [PATCH 229/229] fix: sider changes --- erpnext/support/doctype/issue/issue.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 521e671b0d..086755be51 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -155,7 +155,7 @@ frappe.ui.form.on("Issue", { timeline_refresh: function(frm) { // create button for "Help Article" - if(frappe.model.can_create("Help Article")) { + if (frappe.model.can_create("Help Article")) { // Removing Help Article button if exists to avoid multiple occurance frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove(); $('