-
3. Item Details
+
3. Item Details
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index 79a6aabd987..f4fd06ba037 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -258,7 +258,7 @@
{% } %}
{% } else { %}
{% if(data[i]["party"]|| " ") { %}
- {% if((data[i]["party"]) != __("'Total'")) { %}
+ {% if(!data[i]["is_total_row"]) { %}
{% if(!(filters.customer || filters.supplier)) { %}
{%= data[i]["party"] %}
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 76f3c50578e..0c4a4224407 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -240,8 +240,7 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values():
for entry in entries:
- key = entry.account_number or entry.account_name
- d = accounts_by_name.get(key)
+ d = accounts_by_name.get(entry.account_name)
if d:
for company in companies:
# check if posting date is within the period
@@ -256,7 +255,8 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
- account = d.parent_account.split(' - ')[0].strip()
+ account = d.parent_account_name
+
if not accounts_by_name.get(account):
continue
@@ -267,16 +267,34 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
+
def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
if not accounts:
return None, None
+ accounts = update_parent_account_names(accounts)
+
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name
+def update_parent_account_names(accounts):
+ """Update parent_account_name in accounts list.
+
+ parent_name is `name` of parent account which could have other prefix
+ of account_number and suffix of company abbr. This function adds key called
+ `parent_account_name` which does not have such prefix/suffix.
+ """
+ name_to_account_map = { d.name : d.account_name for d in accounts }
+
+ for account in accounts:
+ if account.parent_account:
+ account["parent_account_name"] = name_to_account_map[account.parent_account]
+
+ return accounts
+
def get_companies(filters):
companies = {}
all_companies = get_subsidiary_companies(filters.get('company'))
@@ -381,9 +399,9 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries:
- key = entry.account_number or entry.account_name
- validate_entries(key, entry, accounts_by_name, accounts)
- gl_entries_by_account.setdefault(key, []).append(entry)
+ account_name = entry.account_name
+ validate_entries(account_name, entry, accounts_by_name, accounts)
+ gl_entries_by_account.setdefault(account_name, []).append(entry)
return gl_entries_by_account
@@ -452,8 +470,7 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
- key = d.account_number or d.account_name
- accounts_by_name[key] = d
+ accounts_by_name[d.account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = []
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index f735d87a764..b5d7992604f 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -129,6 +129,9 @@ def get_gl_entries(filters, accounting_dimensions):
order_by_statement = "order by posting_date, account, creation"
+ if filters.get("include_dimensions"):
+ order_by_statement = "order by posting_date, creation"
+
if filters.get("group_by") == _("Group by Voucher"):
order_by_statement = "order by posting_date, voucher_type, voucher_no"
@@ -142,7 +145,9 @@ def get_gl_entries(filters, accounting_dimensions):
distributed_cost_center_query = ""
if filters and filters.get('cost_center'):
- select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
+ select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit,
+ credit*(DCC_allocation.percentage_allocation/100) as credit,
+ debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
@@ -200,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
- if filters.get("account"):
+ if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
conditions.append("""account in (select name from tabAccount
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
@@ -245,17 +250,19 @@ def get_conditions(filters):
if match_conditions:
conditions.append(match_conditions)
- accounting_dimensions = get_accounting_dimensions(as_list=False)
+ if filters.get("include_dimensions"):
+ accounting_dimensions = get_accounting_dimensions(as_list=False)
- if accounting_dimensions:
- for dimension in accounting_dimensions:
- if filters.get(dimension.fieldname):
- if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'):
- filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type,
- filters.get(dimension.fieldname))
- conditions.append("{0} in %({0})s".format(dimension.fieldname))
- else:
- conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
+ if accounting_dimensions:
+ for dimension in accounting_dimensions:
+ if not dimension.disabled:
+ if filters.get(dimension.fieldname):
+ if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'):
+ filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type,
+ filters.get(dimension.fieldname))
+ conditions.append("{0} in %({0})s".format(dimension.fieldname))
+ else:
+ conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else ""
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index c7cfee74cb0..a8280c1b18e 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -55,7 +55,7 @@ def get_result(filters):
except IndexError:
account = []
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
- filters.company, filters.from_date, filters.to_date)
+ filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
if total_invoiced_amount or tds_deducted:
row = [supplier.pan, supplier.name]
@@ -68,7 +68,7 @@ def get_result(filters):
return out
-def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date):
+def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
''' calculate total invoice amount and total tds deducted for given supplier '''
entries = frappe.db.sql("""
@@ -94,7 +94,9 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date):
""".format(', '.join(["'%s'" % d for d in vouchers])),
(account, from_date, to_date, company))[0][0])
- debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company)
+ date_range_filter = [fiscal_year, from_date, to_date]
+
+ debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 60d1e20feaf..89a05b187d1 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -897,18 +897,18 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
-
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
- gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
+ gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
- voucher_obj = frappe.get_doc(voucher_type, voucher_no)
+ voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
- if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
+ if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
@@ -954,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
return gl_entries
-def compare_existing_and_expected_gle(existing_gle, expected_gle):
+def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
matched = True
for entry in expected_gle:
account_existed = False
for e in existing_gle:
if entry.account == e.account:
account_existed = True
- if entry.account == e.account and entry.against_account == e.against_account \
- and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
- and (entry.debit != e.debit or entry.credit != e.credit):
+ if (entry.account == e.account and entry.against_account == e.against_account
+ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
+ and ( flt(entry.debit, precision) != flt(e.debit, precision) or
+ flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False
break
if not account_existed:
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index 8d24ca82918..fadb66535f5 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -1061,7 +1061,7 @@
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:35.349024",
+ "modified": "2021-03-04 00:38:35.349024",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@@ -1071,7 +1071,7 @@
"pin_to_top": 0,
"shortcuts": [
{
- "label": "Chart Of Accounts",
+ "label": "Chart of Accounts",
"link_to": "Account",
"type": "DocType"
},
@@ -1116,4 +1116,4 @@
"type": "Dashboard"
}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json
index b7d12269c62..a25f5469039 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.json
+++ b/erpnext/assets/doctype/asset_category/asset_category.json
@@ -19,7 +19,6 @@
],
"fields": [
{
- "depends_on": "eval:!doc.asset_category_name",
"fieldname": "asset_category_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -67,7 +66,7 @@
}
],
"links": [],
- "modified": "2021-01-22 12:31:14.425319",
+ "modified": "2021-02-24 15:05:38.621803",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 618212da804..248cb9a8a0e 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -96,7 +96,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 12:00:23.276329",
+ "modified": "2021-03-02 17:34:04.190677",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -113,5 +113,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index d568ef1ceda..02d48653203 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -231,12 +231,12 @@ class TestPurchaseOrder(unittest.TestCase):
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", {
- "item_tax_template": "Test Update Items Template",
+ "item_tax_template": "Test Update Items Template - _TC",
"valid_from": nowdate()
})
new_item_with_tax.save()
- tax_template = "_Test Account Excise Duty @ 10"
+ tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
@@ -287,7 +287,7 @@ class TestPurchaseOrder(unittest.TestCase):
po.cancel()
po.delete()
new_item_with_tax.delete()
- frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
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 75b2954dddc..5baf6939cd3 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -27,11 +27,17 @@
"stock_qty",
"sec_break1",
"price_list_rate",
+ "last_purchase_rate",
+ "col_break3",
+ "base_price_list_rate",
+ "discount_and_margin_section",
+ "margin_type",
+ "margin_rate_or_amount",
+ "rate_with_margin",
+ "column_break_28",
"discount_percentage",
"discount_amount",
- "col_break3",
- "last_purchase_rate",
- "base_price_list_rate",
+ "base_rate_with_margin",
"sec_break2",
"rate",
"amount",
@@ -733,15 +739,59 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "discount_and_margin_section",
+ "fieldtype": "Section Break",
+ "label": "Discount and Margin"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "margin_type",
+ "fieldtype": "Select",
+ "label": "Margin Type",
+ "options": "\nPercentage\nAmount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate",
+ "fieldname": "margin_rate_or_amount",
+ "fieldtype": "Float",
+ "label": "Margin Rate or Amount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "base_rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:44:41.816974",
+ "modified": "2021-02-23 01:00:27.132705",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index a51498e9354..7cf22f87e4f 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -127,6 +127,10 @@ class RequestforQuotation(BuyingController):
'link_doctype': 'Supplier',
'link_name': rfq_supplier.supplier
})
+ contact.append('email_ids', {
+ 'email_id': user.name,
+ 'is_primary': 1
+ })
if not contact.email_id and not contact.user:
contact.email_id = user.name
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 40362b1d404..4cc5753cbd0 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -26,7 +26,6 @@
"supplier_group",
"supplier_type",
"pan",
- "language",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"disabled",
@@ -57,6 +56,7 @@
"website",
"supplier_details",
"column_break_30",
+ "language",
"is_frozen"
],
"fields": [
@@ -384,7 +384,7 @@
"idx": 370,
"image_field": "image",
"links": [],
- "modified": "2020-06-17 23:18:20",
+ "modified": "2021-01-06 19:51:40.939087",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index e4698389963..219d5295c38 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -278,7 +278,7 @@ class BuyingController(StockController):
if self.is_subcontracted == "Yes":
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
- frappe.throw(_("Supplier Warehouse mandatory for sub-contracted Purchase Receipt"))
+ frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom:
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 0e1829a7676..de61b35316e 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -204,8 +204,6 @@ def get_already_returned_items(doc):
return items
def get_returned_qty_map_for_row(row_name, doctype):
- if doctype == "POS Invoice": return {}
-
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
@@ -354,7 +352,12 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
- target_doc.sales_invoice_item = source_doc.name
+
+ if doctype == "Sales Invoice":
+ target_doc.sales_invoice_item = source_doc.name
+ else:
+ target_doc.pos_invoice_item = source_doc.name
+
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index c61b67b0a48..fb52c1f6caa 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -142,6 +142,11 @@ class SellingController(StockController):
self.base_net_total * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
+ if sales_person.commission_rate:
+ sales_person.incentives = flt(
+ sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0,
+ self.precision("incentives", sales_person))
+
total += sales_person.allocated_percentage
if sales_team and total != 100.0:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index ea9659ce017..11ac703311b 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -74,7 +74,7 @@ class StockController(AccountsController):
gl_list = []
warehouse_with_no_account = []
- precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
+ precision = self.get_debit_field_precision()
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
@@ -131,7 +131,13 @@ class StockController(AccountsController):
if frappe.db.get_value("Warehouse", wh, "company"):
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
- return process_gl_map(gl_list)
+ return process_gl_map(gl_list, precision=precision)
+
+ def get_debit_field_precision(self):
+ if not frappe.flags.debit_field_precision:
+ frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
+
+ return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
@@ -244,7 +250,7 @@ class StockController(AccountsController):
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else:
- is_expense_account = frappe.db.get_value("Account",
+ is_expense_account = frappe.get_cached_value("Account",
item.get("expense_account"), "report_type")=="Profit and Loss"
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account:
frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account")
@@ -400,7 +406,8 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
- d.stock_uom_rate = d.rate / d.conversion_factor
+ if d.conversion_factor:
+ d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
@@ -493,7 +500,7 @@ class StockController(AccountsController):
elif not is_reposting_pending():
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
-
+
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index cfa499191ca..aab5770a947 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -15,6 +15,8 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_ra
class calculate_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
+ frappe.flags.round_off_applicable_accounts = []
+ get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def calculate(self):
@@ -107,7 +109,7 @@ class calculate_taxes_and_totals(object):
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
- if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item']:
+ if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
@@ -332,10 +334,18 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
+ current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
+ def get_final_current_tax_amount(self, tax, current_tax_amount):
+ # Some countries need individual tax components to be rounded
+ # Handeled via regional doctypess
+ if tax.account_head in frappe.flags.round_off_applicable_accounts:
+ current_tax_amount = round(current_tax_amount, 0)
+ return current_tax_amount
+
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@@ -693,6 +703,15 @@ def get_itemised_tax_breakup_html(doc):
)
)
+@frappe.whitelist()
+def get_round_off_applicable_accounts(company, account_list):
+ account_list = get_regional_round_off_accounts(company, account_list)
+
+ return account_list
+
+@erpnext.allow_regional
+def get_regional_round_off_accounts(company, account_list):
+ pass
@erpnext.allow_regional
def update_itemised_tax_data(doc):
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 2df1793fdbe..1b33fd73acf 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -49,6 +49,7 @@
"phone",
"mobile_no",
"fax",
+ "website",
"more_info",
"type",
"market_segment",
@@ -56,8 +57,8 @@
"request_type",
"column_break3",
"company",
- "website",
"territory",
+ "language",
"unsubscribed",
"blog_subscriber",
"title"
@@ -447,13 +448,19 @@
"fieldtype": "Select",
"label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther"
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2020-10-13 15:24:00.094811",
+ "modified": "2021-01-06 19:39:58.748978",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 08958b7dd65..ac374a95f4e 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -24,6 +24,12 @@ frappe.ui.form.on("Opportunity", {
frm.trigger('set_contact_link');
}
},
+ contact_date: function(frm) {
+ if(frm.doc.contact_date < frappe.datetime.now_datetime()){
+ frm.set_value("contact_date", "");
+ frappe.throw(__("Next follow up date should be greater than now."))
+ }
+ },
onload_post_render: function(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index eee13f7e799..2e09a76c0f6 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -54,6 +54,7 @@
"campaign",
"column_break1",
"transaction_date",
+ "language",
"amended_from",
"lost_reasons"
],
@@ -419,12 +420,18 @@
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2020-08-12 17:34:35.066961",
+ "modified": "2021-01-06 19:42:46.190051",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index 948e7cc1aed..afa0be9b9f3 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -36,6 +36,7 @@ def enroll_student(source_name):
student.save()
program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name
+ program_enrollment.student_category = student.student_category
program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program")
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
index 9f8f9f4dc00..8180102c582 100644
--- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
+++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
@@ -30,7 +30,7 @@ class ProgramEnrollmentTool(Document):
.format(condition), self.as_dict(), as_dict=1)
elif self.get_students_from == "Program Enrollment":
condition2 = 'and student_batch_name=%(student_batch)s' if self.student_batch else " "
- students = frappe.db.sql('''select student, student_name, student_batch_name from `tabProgram Enrollment`
+ students = frappe.db.sql('''select student, student_name, student_batch_name, student_category from `tabProgram Enrollment`
where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2'''
.format(condition, condition2), self.as_dict(), as_dict=1)
@@ -57,6 +57,7 @@ class ProgramEnrollmentTool(Document):
prog_enrollment = frappe.new_doc("Program Enrollment")
prog_enrollment.student = stud.student
prog_enrollment.student_name = stud.student_name
+ prog_enrollment.student_category = stud.student_category
prog_enrollment.program = self.new_program
prog_enrollment.academic_year = self.new_academic_year
prog_enrollment.academic_term = self.new_academic_term
diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json
index 6df9b9a84f9..95f9224a73c 100644
--- a/erpnext/education/doctype/student_applicant/student_applicant.json
+++ b/erpnext/education/doctype/student_applicant/student_applicant.json
@@ -11,6 +11,7 @@
"middle_name",
"last_name",
"program",
+ "student_category",
"lms_only",
"paid",
"column_break_8",
@@ -257,12 +258,18 @@
"options": "Student Applicant",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "student_category",
+ "fieldtype": "Link",
+ "label": "Student Category",
+ "options": "Student Category"
}
],
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2020-10-05 13:59:45.631647",
+ "modified": "2021-03-01 23:00:25.119241",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Applicant",
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
index cc75a0afbe0..148c1a6a166 100644
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
@@ -117,7 +117,7 @@ def call_mws_method(mws_method, *args, **kwargs):
return response
except Exception as e:
delay = math.pow(4, x) * 125
- frappe.log_error(message=e, title=str(mws_method))
+ frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
time.sleep(delay)
continue
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
index 407f82616ff..8f3b4271c18 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
@@ -103,7 +103,7 @@
}
],
"links": [],
- "modified": "2021-01-29 12:02:16.106942",
+ "modified": "2021-03-02 17:35:14.084342",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Mpesa Settings",
@@ -147,5 +147,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
index 122aa41f4b9..e7176ea945c 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
@@ -70,7 +70,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-10-29 20:24:56.916104",
+ "modified": "2021-03-02 17:35:27.544259",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",
@@ -88,5 +88,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 70c7f3fe5d7..21f6fee79c8 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -204,8 +204,8 @@ def new_bank_transaction(transaction):
"date": getdate(transaction["date"]),
"status": status,
"bank_account": bank_account,
- "debit": debit,
- "credit": credit,
+ "deposit": debit,
+ "withdrawal": credit,
"currency": transaction["iso_currency_code"],
"transaction_id": transaction["transaction_id"],
"reference_number": transaction["payment_meta"]["reference_number"],
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
index 20ec06373e7..308e7d163f3 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
@@ -330,7 +330,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-11-05 20:44:03.664891",
+ "modified": "2021-03-02 17:35:41.953317",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Settings",
@@ -348,5 +348,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
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 30fa23cfb4a..74ad456ea66 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
@@ -17,8 +17,7 @@ class ShopifySettings(unittest.TestCase):
frappe.set_user("Administrator")
# use the fixture data
- import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"),
- ignore_links=True, overwrite=True)
+ import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order")
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
index 15916a5134a..861675acea3 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
@@ -2,4 +2,82 @@
// For license information, please see license.txt
frappe.ui.form.on('Appointment Type', {
+ refresh: function(frm) {
+ frm.set_query('price_list', function() {
+ return {
+ filters: {'selling': 1}
+ };
+ });
+
+ frm.set_query('medical_department', 'items', function(doc) {
+ let item_list = doc.items.map(({medical_department}) => medical_department);
+ return {
+ filters: [
+ ['Medical Department', 'name', 'not in', item_list]
+ ]
+ };
+ });
+
+ frm.set_query('op_consulting_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+
+ frm.set_query('inpatient_visit_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+ }
});
+
+frappe.ui.form.on('Appointment Type Service Item', {
+ op_consulting_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.op_consulting_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.op_consulting_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function(data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ },
+
+ inpatient_visit_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.inpatient_visit_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.inpatient_visit_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function (data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.json b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
index 58753bb4f05..38723182878 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.json
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
@@ -12,7 +12,10 @@
"appointment_type",
"ip",
"default_duration",
- "color"
+ "color",
+ "billing_section",
+ "price_list",
+ "items"
],
"fields": [
{
@@ -52,10 +55,27 @@
"label": "Color",
"no_copy": 1,
"report_hide": 1
+ },
+ {
+ "fieldname": "billing_section",
+ "fieldtype": "Section Break",
+ "label": "Billing"
+ },
+ {
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Appointment Type Service Items",
+ "options": "Appointment Type Service Item"
}
],
"links": [],
- "modified": "2020-02-03 21:06:05.833050",
+ "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
index 1dacffab357..67a24f31e03 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
@@ -4,6 +4,53 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+import frappe
class AppointmentType(Document):
- pass
+ def validate(self):
+ if self.items and self.price_list:
+ for item in self.items:
+ existing_op_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.op_consulting_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
+ make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
+
+ existing_ip_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.inpatient_visit_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
+ make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
+
+@frappe.whitelist()
+def get_service_item_based_on_department(appointment_type, department):
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'medical_department': department, 'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ # if department wise items are not set up
+ # use the generic items
+ if not item_list:
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ return item_list
+
+def make_item_price(price_list, item, item_price):
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'price_list': price_list,
+ 'item_code': item,
+ 'price_list_rate': item_price
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py
rename to erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
new file mode 100644
index 00000000000..5ff68cd682c
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
@@ -0,0 +1,67 @@
+{
+ "actions": [],
+ "creation": "2021-01-22 09:34:53.373105",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "medical_department",
+ "op_consulting_charge_item",
+ "op_consulting_charge",
+ "column_break_4",
+ "inpatient_visit_charge_item",
+ "inpatient_visit_charge"
+ ],
+ "fields": [
+ {
+ "fieldname": "medical_department",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "fieldname": "op_consulting_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "op_consulting_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "inpatient_visit_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "inpatient_visit_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-01-22 09:35:26.503443",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Appointment Type Service Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
similarity index 56%
rename from erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py
rename to erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
index 9840c0dbe37..b2e0e82bad0 100644
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class BankStatementTransactionPaymentItem(Document):
+class AppointmentTypeServiceItem(Document):
pass
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index ff516469eb3..b55d5d6f633 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -364,7 +364,7 @@ let calculate_age = function(birth) {
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
// List Stock items
diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
index cb747f95ef8..8162f03f6dc 100644
--- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
+++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"label": "Out Patient Consulting Charge",
+ "mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency"
},
{
@@ -174,7 +175,8 @@
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
- "label": "Inpatient Visit Charge"
+ "label": "Inpatient Visit Charge",
+ "mandatory_depends_on": "inpatient_visit_charge_item"
},
{
"depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-04-06 13:44:24.759623",
+ "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",
diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js
index f1634c12949..bb7976ccfac 100644
--- a/erpnext/healthcare/doctype/lab_test/lab_test.js
+++ b/erpnext/healthcare/doctype/lab_test/lab_test.js
@@ -258,5 +258,5 @@ var calculate_age = function (dob) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js
index 490f2475001..bce42e51d07 100644
--- a/erpnext/healthcare/doctype/patient/patient.js
+++ b/erpnext/healthcare/doctype/patient/patient.js
@@ -46,11 +46,11 @@ frappe.ui.form.on('Patient', {
}
},
onload: function (frm) {
- if(!frm.doc.dob){
+ if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
- if(frm.doc.dob){
- $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + get_age(frm.doc.dob));
+ if (frm.doc.dob) {
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
}
}
});
@@ -65,7 +65,7 @@ frappe.ui.form.on('Patient', 'dob', function(frm) {
}
else {
let age_str = get_age(frm.doc.dob);
- $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + age_str);
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 63dd8d4793a..8603f974c39 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -108,7 +108,7 @@ class Patient(Document):
if self.dob:
dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), dob)
- age_str = str(age.years) + ' year(s) ' + str(age.months) + ' month(s) ' + str(age.days) + ' day(s)'
+ age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
def invoice_patient_registration(self):
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 3d5073b13e7..2976ef13a1d 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', {
});
frm.set_query('practitioner', function() {
- return {
- filters: {
- 'department': frm.doc.department
- }
- };
+ if (frm.doc.department) {
+ return {
+ filters: {
+ 'department': frm.doc.department
+ }
+ };
+ }
});
frm.set_query('service_unit', function() {
@@ -140,6 +142,20 @@ frappe.ui.form.on('Patient Appointment', {
patient: function(frm) {
if (frm.doc.patient) {
frm.trigger('toggle_payment_fields');
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Patient',
+ name: frm.doc.patient
+ },
+ callback: function (data) {
+ let age = null;
+ if (data.message.dob) {
+ age = calculate_age(data.message.dob);
+ }
+ frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age);
+ }
+ });
} else {
frm.set_value('patient_name', '');
frm.set_value('patient_sex', '');
@@ -148,6 +164,37 @@ frappe.ui.form.on('Patient Appointment', {
}
},
+ practitioner: function(frm) {
+ if (frm.doc.practitioner ) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ appointment_type: function(frm) {
+ if (frm.doc.appointment_type) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ set_payment_details: function(frm) {
+ frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => {
+ if (val) {
+ frappe.call({
+ method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge',
+ args: {
+ doc: frm.doc
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge);
+ frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item);
+ }
+ }
+ });
+ }
+ });
+ },
+
therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter');
},
@@ -190,14 +237,18 @@ frappe.ui.form.on('Patient Appointment', {
// show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0);
+ frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
+ frm.toggle_reqd('billing_item', 0);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', data.message ? 1 : 0);
+ frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
+ frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
});
@@ -540,61 +591,10 @@ let update_status = function(frm, status){
);
};
-frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) {
- if (frm.doc.practitioner) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Healthcare Practitioner',
- name: frm.doc.practitioner
- },
- callback: function (data) {
- frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department);
- frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge);
- frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'patient', function(frm) {
- if (frm.doc.patient) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Patient',
- name: frm.doc.patient
- },
- callback: function (data) {
- let age = null;
- if (data.message.dob) {
- age = calculate_age(data.message.dob);
- }
- frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) {
- if (frm.doc.appointment_type) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Appointment Type',
- name: frm.doc.appointment_type
- },
- callback: function(data) {
- frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration);
- }
- });
- }
-});
-
let calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 35600e48092..83c92af36ac 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -19,19 +19,19 @@
"inpatient_record",
"column_break_1",
"company",
+ "practitioner",
+ "practitioner_name",
+ "department",
"service_unit",
+ "section_break_12",
+ "appointment_type",
+ "duration",
"procedure_template",
"get_procedure_from_encounter",
"procedure_prescription",
"therapy_plan",
"therapy_type",
"get_prescribed_therapies",
- "practitioner",
- "practitioner_name",
- "department",
- "section_break_12",
- "appointment_type",
- "duration",
"column_break_17",
"appointment_date",
"appointment_time",
@@ -79,6 +79,7 @@
"set_only_once": 1
},
{
+ "fetch_from": "appointment_type.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
"in_filter": 1,
@@ -144,7 +145,6 @@
"in_standard_filter": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
- "read_only": 1,
"reqd": 1,
"search_index": 1,
"set_only_once": 1
@@ -158,7 +158,6 @@
"in_standard_filter": 1,
"label": "Department",
"options": "Medical Department",
- "read_only": 1,
"search_index": 1,
"set_only_once": 1
},
@@ -227,12 +226,14 @@
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
- "options": "Mode of Payment"
+ "options": "Mode of Payment",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
- "label": "Paid Amount"
+ "label": "Paid Amount",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "column_break_2",
@@ -302,7 +303,8 @@
"fieldname": "therapy_plan",
"fieldtype": "Link",
"label": "Therapy Plan",
- "options": "Therapy Plan"
+ "options": "Therapy Plan",
+ "set_only_once": 1
},
{
"fieldname": "ref_sales_invoice",
@@ -347,7 +349,7 @@
}
],
"links": [],
- "modified": "2020-12-16 13:16:58.578503",
+ "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index f2b94b8e9c9..1f76cd624cd 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -26,6 +26,7 @@ class PatientAppointment(Document):
def after_insert(self):
self.update_prescription_details()
+ self.set_payment_details()
invoice_appointment(self)
self.update_fee_validity()
send_confirmation_msg(self)
@@ -85,6 +86,13 @@ class PatientAppointment(Document):
def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
+ def set_payment_details(self):
+ if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
+ details = get_service_item_and_practitioner_charge(self)
+ self.db_set('billing_item', details.get('service_item'))
+ if not self.paid_amount:
+ self.db_set('paid_amount', details.get('practitioner_charge'))
+
def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'):
@@ -148,31 +156,37 @@ def invoice_appointment(appointment_doc):
fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity:
- sales_invoice = frappe.new_doc('Sales Invoice')
- sales_invoice.patient = appointment_doc.patient
- sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
- sales_invoice.appointment = appointment_doc.name
- sales_invoice.due_date = getdate()
- sales_invoice.company = appointment_doc.company
- sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
+ create_sales_invoice(appointment_doc)
- item = sales_invoice.append('items', {})
- item = get_appointment_item(appointment_doc, item)
- # Add payments if payment details are supplied else proceed to create invoice as Unpaid
- if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
- sales_invoice.is_pos = 1
- payment = sales_invoice.append('payments', {})
- payment.mode_of_payment = appointment_doc.mode_of_payment
- payment.amount = appointment_doc.paid_amount
+def create_sales_invoice(appointment_doc):
+ sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice.patient = appointment_doc.patient
+ sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
+ sales_invoice.appointment = appointment_doc.name
+ sales_invoice.due_date = getdate()
+ sales_invoice.company = appointment_doc.company
+ sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
- sales_invoice.set_missing_values(for_validate=True)
- sales_invoice.flags.ignore_mandatory = True
- sales_invoice.save(ignore_permissions=True)
- sales_invoice.submit()
- frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
+ item = sales_invoice.append('items', {})
+ item = get_appointment_item(appointment_doc, item)
+
+ # Add payments if payment details are supplied else proceed to create invoice as Unpaid
+ if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
+ sales_invoice.is_pos = 1
+ payment = sales_invoice.append('payments', {})
+ payment.mode_of_payment = appointment_doc.mode_of_payment
+ payment.amount = appointment_doc.paid_amount
+
+ sales_invoice.set_missing_values(for_validate=True)
+ sales_invoice.flags.ignore_mandatory = True
+ sales_invoice.save(ignore_permissions=True)
+ sales_invoice.submit()
+ frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
+ frappe.db.set_value('Patient Appointment', appointment_doc.name, {
+ 'invoiced': 1,
+ 'ref_sales_invoice': sales_invoice.name
+ })
def check_is_new_patient(patient, name=None):
@@ -187,13 +201,14 @@ def check_is_new_patient(patient, name=None):
def get_appointment_item(appointment_doc, item):
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc)
- item.item_code = service_item
+ details = get_service_item_and_practitioner_charge(appointment_doc)
+ charge = appointment_doc.paid_amount or details.get('practitioner_charge')
+ item.item_code = details.get('service_item')
item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company)
item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
- item.rate = practitioner_charge
- item.amount = practitioner_charge
+ item.rate = charge
+ item.amount = charge
item.qty = 1
item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index f7ec6f58fc5..2bb8a53c454 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -32,7 +32,8 @@ class TestPatientAppointment(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
- self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1)
+ appointment.reload()
+ self.assertEqual(appointment.invoiced, 1)
encounter = make_encounter(appointment.name)
self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company)
@@ -41,7 +42,7 @@ class TestPatientAppointment(unittest.TestCase):
# invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
- def test_invoicing(self):
+ def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
@@ -57,6 +58,50 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+ def test_auto_invoicing_based_on_department(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ appointment_type = create_appointment_type()
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, 'HLC-SI-001')
+ self.assertEqual(appointment.paid_amount, 200)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+ self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+
+ def test_auto_invoicing_according_to_appointment_type_charge(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+
+ item = create_healthcare_service_items()
+ items = [{
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 300
+ }]
+ appointment_type = create_appointment_type(args={
+ 'name': 'Generic Appointment Type charge',
+ 'items': items
+ })
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name)
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, item)
+ self.assertEqual(appointment.paid_amount, 300)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@@ -178,14 +223,15 @@ def create_encounter(appointment):
encounter.submit()
return encounter
-def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
+def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
+ service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient
appointment.practitioner = practitioner
- appointment.department = '_Test Medical Department'
+ appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
@@ -193,7 +239,8 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.service_unit = service_unit
if invoice:
appointment.mode_of_payment = 'Cash'
- appointment.paid_amount = 500
+ if appointment_type:
+ appointment.appointment_type = appointment_type
if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
@@ -223,4 +270,29 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
- return template
\ No newline at end of file
+ return template
+
+def create_appointment_type(args=None):
+ if not args:
+ args = frappe.local.form_dict
+
+ name = args.get('name') or 'Test Appointment Type wise Charge'
+
+ if frappe.db.exists('Appointment Type', name):
+ return frappe.get_doc('Appointment Type', name)
+
+ else:
+ item = create_healthcare_service_items()
+ items = [{
+ 'medical_department': '_Test Medical Department',
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 200
+ }]
+ return frappe.get_doc({
+ 'doctype': 'Appointment Type',
+ 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge',
+ 'default_duration': args.get('default_duration') or 20,
+ 'color': args.get('color') or '#7575ff',
+ 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
+ 'items': args.get('items') or items
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
index e960f0a9c40..aaeaa692e63 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
@@ -358,5 +358,5 @@ let calculate_age = function(birth) {
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
index 03903912358..ddf8285bc6d 100644
--- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js
+++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
@@ -36,5 +36,5 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index d4027dff4eb..d3d22c80b67 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -5,9 +5,11 @@
from __future__ import unicode_literals
import math
import frappe
+import json
from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded
+from six import string_types
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
@@ -64,7 +66,9 @@ def get_appointments_to_invoice(patient, company):
income_account = None
service_item = None
if appointment.practitioner:
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment)
+ details = get_service_item_and_practitioner_charge(appointment)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({
'reference_type': 'Patient Appointment',
@@ -97,7 +101,9 @@ def get_encounters_to_invoice(patient, company):
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
+ details = get_service_item_and_practitioner_charge(encounter)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({
@@ -173,7 +179,7 @@ def get_clinical_procedures_to_invoice(patient, company):
if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced:
- service_item = get_healthcare_service_item('clinical_procedure_consumable_item')
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ')
msg += '''Healthcare Settings '''
@@ -304,24 +310,50 @@ def get_therapy_sessions_to_invoice(patient, company):
return therapy_sessions_to_invoice
-
+@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ service_item = None
+ practitioner_charge = None
+ department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department
+
is_inpatient = doc.inpatient_record
- if is_inpatient:
- service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item')
+
+ if doc.get('appointment_type'):
+ service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient)
+
+ if not service_item and not practitioner_charge:
+ service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient)
if not service_item:
- service_item = get_healthcare_service_item('inpatient_visit_charge_item')
- else:
- service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item')
- if not service_item:
- service_item = get_healthcare_service_item('op_consulting_charge_item')
+ service_item = get_healthcare_service_item(is_inpatient)
+
if not service_item:
throw_config_service_item(is_inpatient)
- practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner)
+ return {'service_item': service_item, 'practitioner_charge': practitioner_charge}
+
+
+def get_appointment_type_service_item(appointment_type, department, is_inpatient):
+ from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department
+
+ item_list = get_service_item_based_on_department(appointment_type, department)
+ service_item = None
+ practitioner_charge = None
+
+ if item_list:
+ if is_inpatient:
+ service_item = item_list.get('inpatient_visit_charge_item')
+ practitioner_charge = item_list.get('inpatient_visit_charge')
+ else:
+ service_item = item_list.get('op_consulting_charge_item')
+ practitioner_charge = item_list.get('op_consulting_charge')
+
return service_item, practitioner_charge
@@ -345,12 +377,27 @@ def throw_config_practitioner_charge(is_inpatient, practitioner):
frappe.throw(msg, title=_('Missing Configuration'))
-def get_practitioner_service_item(practitioner, service_item_field):
- return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field)
+def get_practitioner_service_item(practitioner, is_inpatient):
+ service_item = None
+ practitioner_charge = None
+
+ if is_inpatient:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge'])
+ else:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge'])
+
+ return service_item, practitioner_charge
-def get_healthcare_service_item(service_item_field):
- return frappe.db.get_single_value('Healthcare Settings', service_item_field)
+def get_healthcare_service_item(is_inpatient):
+ service_item = None
+
+ if is_inpatient:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item')
+ else:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item')
+
+ return service_item
def get_practitioner_charge(practitioner, is_inpatient):
@@ -381,7 +428,8 @@ def set_invoiced(item, method, ref_invoice=None):
invoiced = True
if item.reference_dt == 'Clinical Procedure':
- if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
+ if service_item == item.item_code:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced)
@@ -403,7 +451,8 @@ def set_invoiced(item, method, ref_invoice=None):
def validate_invoiced_on_submit(item):
- if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ if item.reference_dt == 'Clinical Procedure' and \
+ frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 109d9216e7d..4b3597afd73 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -272,9 +272,15 @@ doc_events = {
'Address': {
'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category']
},
+ 'Supplier': {
+ 'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
+ },
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
+ ('Sales Invoice', 'Purchase Invoice'): {
+ 'validate': ['erpnext.regional.india.utils.validate_document_name']
+ },
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
@@ -355,13 +361,13 @@ scheduler_events = {
"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.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
+ "erpnext.loan_management.doctype.process_loan_interest_accrual.process_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.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
+ "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
]
}
@@ -390,6 +396,15 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
communication_doctypes = ["Customer", "Supplier"]
+accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
+ "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
+ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
+ "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
+ "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
+ "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
+ "Subscription Plan"
+]
+
regional_overrides = {
'France': {
'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'
@@ -399,6 +414,7 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data',
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
+ 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 373b94008e7..18a4fe53c4b 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -131,6 +131,10 @@ def mark_bulk_attendance(data):
data = json.loads(data)
data = frappe._dict(data)
company = frappe.get_value('Employee', data.employee, 'company')
+ if not data.unmarked_days:
+ frappe.throw(_("Please select a date."))
+ return
+
for date in data.unmarked_days:
doc_dict = {
'doctype': 'Attendance',
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 6df3dbd7845..0c7eafe9c61 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -12,7 +12,7 @@ frappe.listview_settings['Attendance'] = {
onload: function(list_view) {
let me = this;
const months = moment.months()
- list_view.page.add_inner_button( __("Mark Attendance"), function(){
+ list_view.page.add_inner_button( __("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
fields: [
@@ -22,11 +22,12 @@ frappe.listview_settings['Attendance'] = {
fieldtype: 'Link',
options: 'Employee',
reqd: 1,
- onchange: function(){
+ onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
}
},
{
@@ -35,13 +36,18 @@ frappe.listview_settings['Attendance'] = {
fieldname: "month",
options: months,
reqd: 1,
- onchange: function(){
+ onchange: function() {
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
});
}
}
@@ -64,21 +70,25 @@ frappe.listview_settings['Attendance'] = {
hidden: 1
},
],
- primary_action(data){
- frappe.confirm(__('Mark attendance as ' + data.status + ' for ' + data.month +' ' + ' on selected dates?'), () => {
- frappe.call({
- method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
- args: {
- data : data
- },
- callback: function(r) {
- if(r.message === 1) {
- frappe.show_alert({message:__("Attendance Marked"), indicator:'blue'});
- cur_dialog.hide();
+ primary_action(data) {
+ if (cur_dialog.no_unmarked_days_left) {
+ frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ } else {
+ frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
+ frappe.call({
+ method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
+ args: {
+ data: data
+ },
+ callback: function(r) {
+ if (r.message === 1) {
+ frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
+ cur_dialog.hide();
+ }
}
- }
+ });
});
- });
+ }
dialog.hide();
list_view.refresh();
},
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index dc2aaa4a067..5123d6a5a78 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2021-01-01 16:54:33.477439",
+ "modified": "2021-01-02 16:54:33.477439",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index c0e614ac088..7d652a7366a 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -48,6 +48,7 @@ class TestEmployee(unittest.TestCase):
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
def make_employee(user, company=None, **kwargs):
+ ""
if not frappe.db.get_value("User", user):
frappe.get_doc({
"doctype": "User",
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index f99963504ab..d8aae667960 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -138,7 +138,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-08-27 14:30:28.995324",
+ "modified": "2021-02-25 12:31:14.947865",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
@@ -155,5 +155,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
-}
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 5e3822e2dad..69d605d0633 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -18,7 +18,6 @@ class ValueMultiplierError(frappe.ValidationError): pass
class LeaveAllocation(Document):
def validate(self):
self.validate_period()
- self.validate_new_leaves_allocated_value()
self.validate_allocation_overlap()
self.validate_back_dated_allocation()
self.set_total_leaves_allocated()
@@ -72,11 +71,6 @@ class LeaveAllocation(Document):
if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"):
frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type))
- def validate_new_leaves_allocated_value(self):
- """validate that leave allocation is in multiples of 0.5"""
- if flt(self.new_leaves_allocated) % 0.5:
- frappe.throw(_("Leaves must be allocated in multiples of 0.5"), ValueMultiplierError)
-
def validate_allocation_overlap(self):
leave_allocation = frappe.db.sql("""
SELECT
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
index 6324b049272..9f667a68356 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
@@ -4,11 +4,11 @@
{{ __("Leave Type") }}
- {{ __("Total Allocated Leaves") }}
- {{ __("Expired Leaves") }}
- {{ __("Used Leaves") }}
- {{ __("Pending Leaves") }}
- {{ __("Available Leaves") }}
+ {{ __("Total Allocated Leave") }}
+ {{ __("Expired Leave") }}
+ {{ __("Used Leave") }}
+ {{ __("Pending Leave") }}
+ {{ __("Available Leave") }}
@@ -25,5 +25,5 @@
{% else %}
-
No Leaves have been allocated.
-{% endif %}
\ No newline at end of file
+
No Leave has been allocated.
+{% endif %}
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index a0327bdaa0b..3373350e733 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -106,12 +106,14 @@
"fieldname": "leaves_allocated",
"fieldtype": "Check",
"hidden": 1,
- "label": "Leaves Allocated"
+ "label": "Leaves Allocated",
+ "no_copy": 1,
+ "print_hide": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-12-31 16:43:30.695206",
+ "modified": "2021-03-01 17:54:01.014509",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index a5068bc26d8..4064c56e44c 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -6,7 +6,7 @@ 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 frappe.utils import getdate, date_diff, comma_and, formatdate, get_datetime, flt
from math import ceil
import json
from six import string_types
@@ -84,17 +84,52 @@ class LeavePolicyAssignment(Document):
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ from frappe.model.meta import get_field_precision
+ precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated"))
+
+ # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
+ if leave_type_details.get(leave_type).is_compensatory == 1:
+ new_leaves_allocated = 0
+
+ elif leave_type_details.get(leave_type).is_earned_leave == 1:
+ if self.assignment_based_on == "Leave Period":
+ new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
+ else:
+ new_leaves_allocated = 0
# 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):
+ elif 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 flt(new_leaves_allocated, precision)
+
+ def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ from erpnext.hr.utils import get_monthly_earned_leave
+
+ current_month = get_datetime().month
+ current_year = get_datetime().year
+
+ from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
+ if getdate(date_of_joining) > getdate(from_date):
+ from_date = date_of_joining
+
+ from_date_month = get_datetime(from_date).month
+ from_date_year = get_datetime(from_date).year
+
+ months_passed = 0
+ if current_year == from_date_year and current_month > from_date_month:
+ months_passed = current_month - from_date_month
+ elif current_year > from_date_year:
+ months_passed = (12 - from_date_month) + current_month
+
+ if months_passed > 0:
+ monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
+ leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
+ new_leaves_allocated = monthly_earned_leave * months_passed
return new_leaves_allocated
+
@frappe.whitelist()
def grant_leave_for_multiple_employees(leave_policy_assignments):
leave_policy_assignments = json.loads(leave_policy_assignments)
@@ -156,7 +191,8 @@ def automatically_allocate_leaves_based_on_leave_policy():
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"])
+ fields=["name", "is_lwp", "is_earned_leave", "is_compensatory",
+ "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index a2092919f8f..fc577ef1d3d 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -172,7 +172,7 @@
"fieldname": "rounding",
"fieldtype": "Select",
"label": "Rounding",
- "options": "0.5\n1.0"
+ "options": "\n0.25\n0.5\n1.0"
},
{
"depends_on": "is_carry_forward",
@@ -197,6 +197,7 @@
"label": "Based On Date Of Joining"
},
{
+ "default": "0",
"depends_on": "eval:doc.is_lwp == 0",
"fieldname": "is_ppl",
"fieldtype": "Check",
@@ -213,7 +214,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
- "modified": "2020-10-15 15:49:47.555105",
+ "modified": "2021-03-02 11:22:33.776320",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
diff --git a/erpnext/hr/doctype/skill/skill.json b/erpnext/hr/doctype/skill/skill.json
index 518297395bd..4c8a8c92c10 100644
--- a/erpnext/hr/doctype/skill/skill.json
+++ b/erpnext/hr/doctype/skill/skill.json
@@ -3,7 +3,7 @@
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
- "allow_rename": 0,
+ "allow_rename": 1,
"autoname": "field:skill_name",
"beta": 0,
"creation": "2019-04-16 09:54:39.486915",
@@ -16,7 +16,7 @@
"fields": [
{
"allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
+ "allow_in_quick_entry": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -46,6 +46,12 @@
"set_only_once": 0,
"translatable": 0,
"unique": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description"
}
],
"has_web_view": 0,
@@ -56,7 +62,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2019-04-16 09:55:00.536328",
+ "modified": "2021-02-26 10:55:00.536328",
"modified_by": "Administrator",
"module": "HR",
"name": "Skill",
@@ -110,4 +116,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/page/team_updates/team_updates.js b/erpnext/hr/page/team_updates/team_updates.js
index 13d0074660b..358329748e6 100644
--- a/erpnext/hr/page/team_updates/team_updates.js
+++ b/erpnext/hr/page/team_updates/team_updates.js
@@ -36,7 +36,7 @@ frappe.team_updates = {
start: me.start
},
callback: function(r) {
- if(r.message) {
+ if (r.message && r.message.length > 0) {
r.message.forEach(function(d) {
me.add_row(d);
});
@@ -75,6 +75,6 @@ frappe.team_updates = {
}
me.last_feed_date = date;
- $(frappe.render_template('team_update_row', data)).appendTo(me.body)
+ $(frappe.render_template('team_update_row', data)).appendTo(me.body);
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 1b923581841..06f9160363c 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -40,17 +40,17 @@ def get_columns():
'fieldname': 'opening_balance',
'width': 130,
}, {
- 'label': _('Leaves Allocated'),
+ 'label': _('Leave Allocated'),
'fieldtype': 'float',
'fieldname': 'leaves_allocated',
'width': 130,
}, {
- 'label': _('Leaves Taken'),
+ 'label': _('Leave Taken'),
'fieldtype': 'float',
'fieldname': 'leaves_taken',
'width': 130,
}, {
- 'label': _('Leaves Expired'),
+ 'label': _('Leave Expired'),
'fieldtype': 'float',
'fieldname': 'leaves_expired',
'width': 130,
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index e2aa7a4e727..0c4c1cafb07 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-import frappe, erpnext
-from frappe import _
-from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today
-from frappe.model.document import Document
-from frappe.desk.form import assign_to
+import erpnext
+import frappe
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from frappe import _
+from frappe.desk.form import assign_to
+from frappe.model.document import Document
+from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
+ get_datetime, getdate, nowdate, today, unique)
+
class DuplicateDeclarationError(frappe.ValidationError): pass
+
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
@@ -48,27 +51,38 @@ class EmployeeBoardingController(Document):
continue
task = frappe.get_doc({
- "doctype": "Task",
- "project": self.project,
- "subject": activity.activity_name + " : " + self.employee_name,
- "description": activity.description,
- "department": self.department,
- "company": self.company,
- "task_weight": activity.task_weight
- }).insert(ignore_permissions=True)
+ "doctype": "Task",
+ "project": self.project,
+ "subject": activity.activity_name + " : " + self.employee_name,
+ "description": activity.description,
+ "department": self.department,
+ "company": self.company,
+ "task_weight": activity.task_weight
+ }).insert(ignore_permissions=True)
activity.db_set("task", task.name)
+
users = [activity.user] if activity.user else []
if activity.role:
- user_list = frappe.db.sql_list('''select distinct(parent) from `tabHas Role`
- where parenttype='User' and role=%s''', activity.role)
- users = users + user_list
+ user_list = frappe.db.sql_list('''
+ SELECT
+ DISTINCT(has_role.parent)
+ FROM
+ `tabHas Role` has_role
+ LEFT JOIN `tabUser` user
+ ON has_role.parent = user.name
+ WHERE
+ has_role.parenttype = 'User'
+ AND user.enabled = 1
+ AND has_role.role = %s
+ ''', activity.role)
+ users = unique(users + user_list)
if "Administrator" in users:
users.remove("Administrator")
# assign the task the users
if users:
- self.assign_task_to_users(task, set(users))
+ self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
@@ -316,13 +330,7 @@ def allocate_earned_leaves():
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)
+ earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
@@ -335,6 +343,21 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type
today_date = today()
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+def get_monthly_earned_leave(annual_leaves, frequency, rounding):
+ earned_leaves = 0.0
+ divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
+ if annual_leaves:
+ earned_leaves = flt(annual_leaves) / divide_by_frequency[frequency]
+ if rounding:
+ if rounding == "0.25":
+ earned_leaves = round(earned_leaves * 4) / 4
+ elif rounding == "0.5":
+ earned_leaves = round(earned_leaves * 2) / 2
+ else:
+ earned_leaves = round(earned_leaves)
+
+ return earned_leaves
+
def get_leave_allocations(date, leave_type):
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
diff --git a/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json
new file mode 100644
index 00000000000..b8abf210f81
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json
@@ -0,0 +1,29 @@
+{
+ "based_on": "disbursement_date",
+ "chart_name": "Loan Disbursements",
+ "chart_type": "Sum",
+ "creation": "2021-02-06 18:40:36.148470",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan Disbursement",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2021-02-06 18:40:49.308663",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Disbursements",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Month",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "disbursed_amount",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json
new file mode 100644
index 00000000000..aa0f78a2f6e
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "posting_date",
+ "chart_name": "Loan Interest Accrual",
+ "chart_type": "Sum",
+ "color": "#39E4A5",
+ "creation": "2021-02-18 20:07:04.843876",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan Interest Accrual",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Interest Accrual\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 21:01:26.022634",
+ "modified": "2021-02-21 21:01:44.930712",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Interest Accrual",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Monthly",
+ "timeseries": 1,
+ "timespan": "Last Year",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "interest_amount",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json
new file mode 100644
index 00000000000..35bd43b994f
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "creation",
+ "chart_name": "New Loans",
+ "chart_type": "Count",
+ "color": "#449CF0",
+ "creation": "2021-02-06 16:59:27.509170",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 20:55:33.515025",
+ "modified": "2021-02-21 21:00:33.900821",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loans",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Month",
+ "type": "Bar",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
new file mode 100644
index 00000000000..76c27b062d6
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "",
+ "chart_name": "Top 10 Pledged Loan Securities",
+ "chart_type": "Custom",
+ "color": "#EC864B",
+ "creation": "2021-02-06 22:02:46.284479",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 21:00:57.043034",
+ "modified": "2021-02-21 21:01:10.048623",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Top 10 Pledged Loan Securities",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "Top 10 Pledged Loan Securities",
+ "time_interval": "Yearly",
+ "timeseries": 0,
+ "timespan": "Last Year",
+ "type": "Bar",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py b/erpnext/loan_management/dashboard_chart_source/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py
rename to erpnext/loan_management/dashboard_chart_source/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py
rename to erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js
new file mode 100644
index 00000000000..cf75cc8e41a
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js
@@ -0,0 +1,14 @@
+frappe.provide('frappe.dashboards.chart_sources');
+
+frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = {
+ method: "erpnext.loan_management.dashboard_chart_source.top_10_pledged_loan_securities.top_10_pledged_loan_securities.get_data",
+ filters: [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company")
+ }
+ ]
+};
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
new file mode 100644
index 00000000000..42c9b1c335a
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
@@ -0,0 +1,13 @@
+{
+ "creation": "2021-02-06 22:01:01.332628",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart Source",
+ "idx": 0,
+ "modified": "2021-02-06 22:01:01.332628",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Top 10 Pledged Loan Securities",
+ "owner": "Administrator",
+ "source_name": "Top 10 Pledged Loan Securities ",
+ "timeseries": 0
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
new file mode 100644
index 00000000000..6bb04401bed
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils.dashboard import cache_source
+from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
+ import get_loan_security_details
+from six import iteritems
+
+@frappe.whitelist()
+@cache_source
+def get_data(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
+ to_date = None, timespan = None, time_interval = None, heatmap_year = None):
+ if chart_name:
+ chart = frappe.get_doc('Dashboard Chart', chart_name)
+ else:
+ chart = frappe._dict(frappe.parse_json(chart))
+
+ filters = {}
+ current_pledges = {}
+
+ if filters:
+ filters = frappe.parse_json(filters)[0]
+
+ conditions = ""
+ labels = []
+ values = []
+
+ if filters.get('company'):
+ conditions = "AND company = %(company)s"
+
+ loan_security_details = get_loan_security_details()
+
+ unpledges = frappe._dict(frappe.db.sql("""
+ SELECT u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE u.parent = up.name
+ AND up.status = 'Approved'
+ {conditions}
+ GROUP BY u.loan_security
+ """.format(conditions=conditions), filters, as_list=1))
+
+ pledges = frappe._dict(frappe.db.sql("""
+ SELECT p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE p.parent = lp.name
+ AND lp.status = 'Pledged'
+ {conditions}
+ GROUP BY p.loan_security
+ """.format(conditions=conditions), filters, as_list=1))
+
+ for security, qty in iteritems(pledges):
+ current_pledges.setdefault(security, qty)
+ current_pledges[security] -= unpledges.get(security, 0.0)
+
+ sorted_pledges = dict(sorted(current_pledges.items(), key=lambda item: item[1], reverse=True))
+
+ count = 0
+ for security, qty in iteritems(sorted_pledges):
+ values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0))
+ labels.append(security)
+ count +=1
+
+ ## Just need top 10 securities
+ if count == 10:
+ break
+
+ return {
+ 'labels': labels,
+ 'datasets': [{
+ 'name': 'Top 10 Securities',
+ 'chartType': 'bar',
+ 'values': values
+ }]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index e607d4f3cbf..83a813f947b 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -201,7 +201,9 @@ def request_loan_closure(loan, posting_date=None):
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 < write_off_limit:
+ if not pending_amount:
+ frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+ elif pending_amount < write_off_limit:
# Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan)
write_off.submit()
@@ -348,3 +350,13 @@ def validate_employee_currency_with_company_currency(applicant, 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))
+
+@frappe.whitelist()
+def get_shortfall_applicants():
+ loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan')
+ applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name'))
+
+ return {
+ "value": len(applicants),
+ "fieldtype": "Int"
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index f3c9db62338..13a209418d1 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -547,7 +547,7 @@ class TestLoan(unittest.TestCase):
# 30 days - grace period
penalty_days = 30 - 4
- penalty_applicable_amount = flt(amounts['interest_amount']/2, 2)
+ penalty_applicable_amount = flt(amounts['interest_amount']/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')
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index e59db4c12dc..9c0147e55ba 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -197,7 +197,7 @@ def get_proposed_pledge(securities):
security.qty = cint(security.amount/security.loan_security_price)
security.amount = security.qty * security.loan_security_price
- security.post_haircut_amount = security.amount - (security.amount * security.haircut/100)
+ security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100))
maximum_loan_amount += security.post_haircut_amount
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 7d7992d40ae..7978350adf8 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
@@ -246,7 +246,5 @@ 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), precision)
+ return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index ac30c91b670..bac06c4e9e6 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -81,8 +81,8 @@ class LoanRepayment(AccountsController):
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,
- self.rate_of_interest, self.posting_date), 2)
+ per_day_interest = get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date)
no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
precision)/per_day_interest, 0) - 1
@@ -105,8 +105,6 @@ class LoanRepayment(AccountsController):
})
def update_paid_amount(self):
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
loan = frappe.get_doc("Loan", self.against_loan)
for payment in self.repayment_details:
@@ -114,7 +112,7 @@ class LoanRepayment(AccountsController):
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
- (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
+ (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
@@ -148,8 +146,6 @@ class LoanRepayment(AccountsController):
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
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
@@ -185,21 +181,18 @@ class LoanRepayment(AccountsController):
# 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)
+ per_day_interest = get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date)
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
@@ -369,7 +362,7 @@ def get_amounts(amounts, against_loan, posting_date):
if pending_days > 0:
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))
+ unaccrued_interest += (pending_days * per_day_interest)
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
diff --git a/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json
new file mode 100644
index 00000000000..e060253d34c
--- /dev/null
+++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json
@@ -0,0 +1,70 @@
+{
+ "cards": [
+ {
+ "card": "New Loans"
+ },
+ {
+ "card": "Active Loans"
+ },
+ {
+ "card": "Closed Loans"
+ },
+ {
+ "card": "Total Disbursed"
+ },
+ {
+ "card": "Open Loan Applications"
+ },
+ {
+ "card": "New Loan Applications"
+ },
+ {
+ "card": "Total Sanctioned Amount"
+ },
+ {
+ "card": "Active Securities"
+ },
+ {
+ "card": "Applicants With Unpaid Shortfall"
+ },
+ {
+ "card": "Total Shortfall Amount"
+ },
+ {
+ "card": "Total Repayment"
+ },
+ {
+ "card": "Total Write Off"
+ }
+ ],
+ "charts": [
+ {
+ "chart": "New Loans",
+ "width": "Half"
+ },
+ {
+ "chart": "Loan Disbursements",
+ "width": "Half"
+ },
+ {
+ "chart": "Top 10 Pledged Loan Securities",
+ "width": "Half"
+ },
+ {
+ "chart": "Loan Interest Accrual",
+ "width": "Half"
+ }
+ ],
+ "creation": "2021-02-06 16:52:43.484752",
+ "dashboard_name": "Loan Dashboard",
+ "docstatus": 0,
+ "doctype": "Dashboard",
+ "idx": 0,
+ "is_default": 0,
+ "is_standard": 1,
+ "modified": "2021-02-21 20:53:47.531699",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Dashboard",
+ "owner": "Administrator"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/active_loans/active_loans.json b/erpnext/loan_management/number_card/active_loans/active_loans.json
new file mode 100644
index 00000000000..7e0db472882
--- /dev/null
+++ b/erpnext/loan_management/number_card/active_loans/active_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:10:26.132493",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"in\",[\"Disbursed\",\"Partially Disbursed\",null],false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Active Loans",
+ "modified": "2021-02-06 17:29:20.304087",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Active Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/active_securities/active_securities.json b/erpnext/loan_management/number_card/active_securities/active_securities.json
new file mode 100644
index 00000000000..298e41061a8
--- /dev/null
+++ b/erpnext/loan_management/number_card/active_securities/active_securities.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 19:07:21.344199",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Security",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Security\",\"disabled\",\"=\",0,false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Active Securities",
+ "modified": "2021-02-06 19:07:26.671516",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Active Securities",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json
new file mode 100644
index 00000000000..3b9eba15536
--- /dev/null
+++ b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json
@@ -0,0 +1,21 @@
+{
+ "creation": "2021-02-07 18:55:12.632616",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "filters_json": "null",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Applicants With Unpaid Shortfall",
+ "method": "erpnext.loan_management.doctype.loan.loan.get_shortfall_applicants",
+ "modified": "2021-02-07 21:46:27.369795",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Applicants With Unpaid Shortfall",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Custom"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/closed_loans/closed_loans.json b/erpnext/loan_management/number_card/closed_loans/closed_loans.json
new file mode 100644
index 00000000000..c2f22442653
--- /dev/null
+++ b/erpnext/loan_management/number_card/closed_loans/closed_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-21 19:51:49.261813",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Closed\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Closed Loans",
+ "modified": "2021-02-21 19:51:54.087903",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Closed Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json
new file mode 100644
index 00000000000..65c8ce67d21
--- /dev/null
+++ b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json
@@ -0,0 +1,21 @@
+{
+ "creation": "2021-02-07 21:57:14.758007",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "filters_json": "null",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Last Interest Accrual",
+ "method": "erpnext.loan_management.doctype.loan.loan.get_last_accrual_date",
+ "modified": "2021-02-07 21:59:47.525197",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Last Interest Accrual",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Custom"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json
new file mode 100644
index 00000000000..7e655ff35c7
--- /dev/null
+++ b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:59:10.051269",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Application",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"creation\",\"Timespan\",\"today\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "New Loan Applications",
+ "modified": "2021-02-06 17:59:21.880979",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loan Applications",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/new_loans/new_loans.json b/erpnext/loan_management/number_card/new_loans/new_loans.json
new file mode 100644
index 00000000000..424f0f14958
--- /dev/null
+++ b/erpnext/loan_management/number_card/new_loans/new_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:56:34.624031",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"creation\",\"Timespan\",\"today\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "New Loans",
+ "modified": "2021-02-06 17:58:20.209166",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json
new file mode 100644
index 00000000000..1d5e84ed7f0
--- /dev/null
+++ b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:23:32.509899",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Application",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"status\",\"=\",\"Open\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Open Loan Applications",
+ "modified": "2021-02-06 17:29:09.761011",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Open Loan Applications",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json
new file mode 100644
index 00000000000..4a3f8699a04
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "disbursed_amount",
+ "creation": "2021-02-06 16:52:19.505462",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Disbursement",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Disbursed Amount",
+ "modified": "2021-02-06 17:29:38.453870",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Disbursed",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_repayment/total_repayment.json b/erpnext/loan_management/number_card/total_repayment/total_repayment.json
new file mode 100644
index 00000000000..38de42b89c8
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_repayment/total_repayment.json
@@ -0,0 +1,24 @@
+{
+ "aggregate_function_based_on": "amount_paid",
+ "color": "#29CD42",
+ "creation": "2021-02-21 19:27:45.989222",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Repayment",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Repayment\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Repayment",
+ "modified": "2021-02-21 19:34:59.656546",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Repayment",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json
new file mode 100644
index 00000000000..dfb9d24e925
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "loan_amount",
+ "creation": "2021-02-06 17:05:04.704162",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Sanctioned\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Sanctioned Amount",
+ "modified": "2021-02-06 17:29:29.930557",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Sanctioned Amount",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json
new file mode 100644
index 00000000000..aa6b0937323
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "shortfall_amount",
+ "creation": "2021-02-09 08:07:20.096995",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Security Shortfall",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Unpaid Shortfall Amount",
+ "modified": "2021-02-09 08:09:00.355547",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Shortfall Amount",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_write_off/total_write_off.json b/erpnext/loan_management/number_card/total_write_off/total_write_off.json
new file mode 100644
index 00000000000..c85169acf8d
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_write_off/total_write_off.json
@@ -0,0 +1,24 @@
+{
+ "aggregate_function_based_on": "write_off_amount",
+ "color": "#CB2929",
+ "creation": "2021-02-21 19:48:29.004429",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Write Off",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Write Off\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Write Off",
+ "modified": "2021-02-21 19:48:58.604159",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Write Off",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
index ab586bc09c4..0ccd149e5fb 100644
--- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
@@ -36,7 +36,7 @@ def get_columns(filters):
def get_data(filters):
data = []
- loan_security_details = get_loan_security_details(filters)
+ loan_security_details = get_loan_security_details()
pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
loan_security_details)
@@ -64,7 +64,7 @@ def get_data(filters):
return data
-def get_loan_security_details(filters):
+def get_loan_security_details():
security_detail_map = {}
loan_security_price_map = {}
lsp_validity_map = {}
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
index a3e69bbfbfc..0f72c3cce7c 100644
--- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -171,7 +171,7 @@ def get_loan_wise_pledges(filters):
return current_pledges
def get_loan_wise_security_value(filters, current_pledges):
- loan_security_details = get_loan_security_details(filters)
+ loan_security_details = get_loan_security_details()
loan_wise_security_value = {}
for key in current_pledges:
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
index adc8013c686..887a86a46c5 100644
--- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
@@ -35,7 +35,7 @@ def get_columns(filters):
def get_data(filters):
data = []
- loan_security_details = get_loan_security_details(filters)
+ loan_security_details = get_loan_security_details()
current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details)
currency = erpnext.get_company_currency(filters.get('company'))
@@ -76,7 +76,7 @@ def get_company_wise_loan_security_details(filters, loan_security_details):
if qty:
security_wise_map[key[1]]['applicant_count'] += 1
- total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price'])
+ total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
return security_wise_map, total_portfolio_value
diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json
index 2e8b5bf5b31..18559dceef7 100644
--- a/erpnext/loan_management/workspace/loan_management/loan_management.json
+++ b/erpnext/loan_management/workspace/loan_management/loan_management.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "loan",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Loan Management",
"links": [
@@ -219,7 +220,7 @@
"type": "Link"
}
],
- "modified": "2021-01-12 11:27:56.079724",
+ "modified": "2021-02-18 17:31:53.586508",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Management",
@@ -239,6 +240,12 @@
"label": "Loan",
"link_to": "Loan",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "label": "Dashboard",
+ "link_to": "Loan Dashboard",
+ "type": "Dashboard"
}
]
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index ec28eb7795c..662a06b1ee2 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -267,6 +267,17 @@ class JobCard(Document):
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
+ def set_transferred_qty_in_job_card(self, ste_doc):
+ for row in ste_doc.items:
+ if not row.job_card_item: continue
+
+ qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
+ WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
+ se.purpose = 'Material Transfer for Manufacture'
+ """, (row.job_card_item))[0][0]
+
+ frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
+
def set_transferred_qty(self, update_status=False):
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -279,7 +290,8 @@ class JobCard(Document):
self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name,
'work_order': self.work_order,
- 'docstatus': 1
+ 'docstatus': 1,
+ 'purpose': 'Material Transfer for Manufacture'
}, 'sum(fg_completed_qty)') or 0
self.db_set("transferred_qty", self.transferred_qty)
@@ -420,6 +432,7 @@ def make_stock_entry(source_name, target_doc=None):
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+ target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()
target.set_stock_entry_type()
@@ -437,9 +450,10 @@ def make_stock_entry(source_name, target_doc=None):
"field_map": {
"source_warehouse": "s_warehouse",
"required_qty": "qty",
- "uom": "stock_uom"
+ "name": "job_card_item"
},
"postprocess": update_item,
+ "condition": lambda doc: doc.required_qty > 0
}
}, target_doc, set_missing_values)
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index bc9fe108ca6..100ef4ca3a3 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -1,363 +1,120 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-07-09 17:20:44.737289",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2018-07-09 17:20:44.737289",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "source_warehouse",
+ "uom",
+ "item_group",
+ "column_break_3",
+ "stock_uom",
+ "item_name",
+ "description",
+ "qty_section",
+ "required_qty",
+ "column_break_9",
+ "transferred_qty",
+ "allow_alternative_item"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_code",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Item Code",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "source_warehouse",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 1,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Source Warehouse",
- "length": 0,
- "no_copy": 0,
- "options": "Warehouse",
- "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": "source_warehouse",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "uom",
- "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": "UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
{
- "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,
- "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_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item Name",
- "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
- },
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "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": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "qty_section",
- "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": "Qty",
- "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": "qty_section",
+ "fieldtype": "Section Break",
+ "label": "Qty"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "required_qty",
- "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": "Required Qty",
- "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
- },
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Required Qty",
+ "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": "allow_alternative_item",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allow Alternative Item",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "allow_alternative_item",
+ "fieldtype": "Check",
+ "label": "Allow Alternative Item"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "transferred_qty",
+ "fieldtype": "Float",
+ "label": "Transferred Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-08-28 15:23:48.099459",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Job Card Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "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
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-11 13:50:13.804108",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 06a8e1987d3..00e8c5418a0 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1)
- bin1_on_submit = get_bin(item, warehouse)
+ reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production)
# reserved qty for production is updated
- self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2,
- cint(bin1_on_submit.reserved_qty_for_production))
+ self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
+
test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100)
@@ -109,9 +109,9 @@ class TestWorkOrder(unittest.TestCase):
s.submit()
bin1_at_completion = get_bin(item, warehouse)
-
+
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production),
- cint(bin1_on_submit.reserved_qty_for_production) - 1)
+ reserved_qty_on_submission - 1)
def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index ca530bbaddc..3d64ad4318d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -528,6 +528,10 @@ class WorkOrder(Document):
if not reset_only_qty:
self.required_items = []
+ operation = None
+ if self.get('operations') and len(self.operations) == 1:
+ operation = self.operations[0].operation
+
if self.bom_no and self.qty:
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty,
fetch_exploded = self.use_multi_level_bom)
@@ -536,6 +540,9 @@ class WorkOrder(Document):
for d in self.get("required_items"):
if item_dict.get(d.item_code):
d.required_qty = item_dict.get(d.item_code).get("qty")
+
+ if not d.operation:
+ d.operation = operation
else:
# Attribute a big number (999) to idx for sorting putpose in case idx is NULL
# For instance in BOM Explosion Item child table, the items coming from sub assembly items
@@ -543,7 +550,7 @@ class WorkOrder(Document):
self.append('required_items', {
'rate': item.rate,
'amount': item.amount,
- 'operation': item.operation,
+ 'operation': item.operation or operation,
'item_code': item.item_code,
'item_name': item.item_name,
'description': item.description,
@@ -879,7 +886,7 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto
doc.schedule_time_logs(row)
doc.insert()
- frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)))
+ frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
return doc
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index f7b407b7922..ffd9242e1b8 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -88,11 +88,11 @@ def get_bom_stock(filters):
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"])
+ details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
- return manufacture_details
\ No newline at end of file
+ return manufacture_details
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py
rename to erpnext/non_profit/doctype/donation/__init__.py
diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js
new file mode 100644
index 00000000000..10e82201440
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.js
@@ -0,0 +1,26 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Donation', {
+ refresh: function(frm) {
+ if (frm.doc.docstatus === 1 && !frm.doc.paid) {
+ frm.add_custom_button(__('Create Payment Entry'), function() {
+ frm.events.make_payment_entry(frm);
+ });
+ }
+ },
+
+ make_payment_entry: function(frm) {
+ return frappe.call({
+ method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
+ args: {
+ 'dt': frm.doc.doctype,
+ 'dn': frm.doc.name
+ },
+ callback: function(r) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route('Form', doc[0].doctype, doc[0].name);
+ }
+ });
+ },
+});
diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json
new file mode 100644
index 00000000000..6759569d54d
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.json
@@ -0,0 +1,156 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2021-02-17 10:28:52.645731",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "donor",
+ "donor_name",
+ "email",
+ "column_break_4",
+ "company",
+ "date",
+ "payment_details_section",
+ "paid",
+ "amount",
+ "mode_of_payment",
+ "razorpay_payment_id",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "donor",
+ "fieldtype": "Link",
+ "label": "Donor",
+ "options": "Donor",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "donor.donor_name",
+ "fieldname": "donor_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Donor Name",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donor.email",
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "payment_details_section",
+ "fieldtype": "Section Break",
+ "label": "Payment Details"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "Razorpay Payment ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "NPO-DTN-.YYYY.-"
+ },
+ {
+ "default": "0",
+ "fieldname": "paid",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Paid"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Donation",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-03-11 10:53:11.269005",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Donation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "donor_name, email",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "donor_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py
new file mode 100644
index 00000000000..e947588482d
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import six
+import json
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import getdate, flt, get_link_to_form
+from frappe.email import sendmail_to_system_managers
+from erpnext.non_profit.doctype.membership.membership import verify_signature
+
+class Donation(Document):
+ def validate(self):
+ if not self.donor or not frappe.db.exists('Donor', self.donor):
+ # for web forms
+ user_type = frappe.db.get_value('User', frappe.session.user, 'user_type')
+ if user_type == 'Website User':
+ self.create_donor_for_website_user()
+ else:
+ frappe.throw(_('Please select a Member'))
+
+ def create_donor_for_website_user(self):
+ donor_name = frappe.get_value('Donor', dict(email=frappe.session.user))
+
+ if not donor_name:
+ user = frappe.get_doc('User', frappe.session.user)
+ donor = frappe.get_doc(dict(
+ doctype='Donor',
+ donor_type=self.get('donor_type'),
+ email=frappe.session.user,
+ member_name=user.get_fullname()
+ )).insert(ignore_permissions=True)
+ donor_name = donor.name
+
+ if self.get('__islocal'):
+ self.donor = donor_name
+
+ def on_payment_authorized(self, *args, **kwargs):
+ self.load_from_db()
+ self.create_payment_entry()
+
+ def create_payment_entry(self):
+ settings = frappe.get_doc('Non Profit Settings')
+ if not settings.automate_donation_payment_entries:
+ return
+
+ if not settings.donation_payment_account:
+ frappe.throw(_('You need to set
Payment Account for Donation in {0}').format(
+ get_link_to_form('Non Profit Settings', 'Non Profit Settings')))
+
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+ frappe.flags.ignore_account_permission = True
+ pe = get_payment_entry(dt=self.doctype, dn=self.name)
+ frappe.flags.ignore_account_permission = False
+ pe.paid_from = settings.donation_debit_account
+ pe.paid_to = settings.donation_payment_account
+ pe.reference_no = self.name
+ pe.reference_date = getdate()
+ pe.flags.ignore_mandatory = True
+ pe.insert()
+ pe.submit()
+
+
+@frappe.whitelist(allow_guest=True)
+def capture_razorpay_donations(*args, **kwargs):
+ """
+ Creates Donation from Razorpay Webhook Request Data on payment.captured event
+ Creates Donor from email if not found
+ """
+ data = frappe.request.get_data(as_text=True)
+
+ try:
+ verify_signature(data, endpoint='Donation')
+ except Exception as e:
+ log = frappe.log_error(e, 'Donation Webhook Verification Error')
+ notify_failure(log)
+ return { 'status': 'Failed', 'reason': e }
+
+ if isinstance(data, six.string_types):
+ data = json.loads(data)
+ data = frappe._dict(data)
+
+ payment = data.payload.get('payment', {}).get('entity', {})
+ payment = frappe._dict(payment)
+
+ try:
+ if not data.event == 'payment.captured':
+ return
+
+ donor = get_donor(payment.email)
+ if not donor:
+ donor = create_donor(payment)
+
+ donation = create_donation(donor, payment)
+ donation.run_method('create_payment_entry')
+
+ except Exception as e:
+ message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id)
+ log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name))
+ notify_failure(log)
+ return { 'status': 'Failed', 'reason': e }
+
+ return { 'status': 'Success' }
+
+
+def create_donation(donor, payment):
+ if not frappe.db.exists('Mode of Payment', payment.method):
+ create_mode_of_payment(payment.method)
+
+ company = get_company_for_donations()
+ donation = frappe.get_doc({
+ 'doctype': 'Donation',
+ 'company': company,
+ 'donor': donor.name,
+ 'donor_name': donor.donor_name,
+ 'email': donor.email,
+ 'date': getdate(),
+ 'amount': flt(payment.amount),
+ 'mode_of_payment': payment.method,
+ 'razorpay_payment_id': payment.id
+ }).insert(ignore_mandatory=True)
+
+ donation.submit()
+ return donation
+
+
+def get_donor(email):
+ donors = frappe.get_all('Donor',
+ filters={'email': email},
+ order_by='creation desc')
+
+ try:
+ return frappe.get_doc('Donor', donors[0]['name'])
+ except Exception:
+ return None
+
+
+@frappe.whitelist()
+def create_donor(payment):
+ donor_details = frappe._dict(payment)
+ donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type')
+
+ donor = frappe.new_doc('Donor')
+ donor.update({
+ 'donor_name': donor_details.email,
+ 'donor_type': donor_type,
+ 'email': donor_details.email,
+ 'contact': donor_details.contact
+ })
+
+ if donor_details.get('notes'):
+ donor = get_additional_notes(donor, donor_details)
+
+ donor.insert(ignore_mandatory=True)
+ return donor
+
+
+def get_company_for_donations():
+ company = frappe.db.get_single_value('Non Profit Settings', 'donation_company')
+ if not company:
+ from erpnext.healthcare.setup import get_company
+ company = get_company()
+ return company
+
+
+def get_additional_notes(donor, donor_details):
+ if type(donor_details.notes) == dict:
+ for k, v in donor_details.notes.items():
+ notes = '\n'.join('{}: {}'.format(k, v))
+
+ # extract donor name from notes
+ if 'name' in k.lower():
+ donor.update({
+ 'donor_name': donor_details.notes.get(k)
+ })
+
+ # extract pan from notes
+ if 'pan' in k.lower():
+ donor.update({
+ 'pan_number': donor_details.notes.get(k)
+ })
+
+ donor.add_comment('Comment', notes)
+
+ elif type(donor_details.notes) == str:
+ donor.add_comment('Comment', donor_details.notes)
+
+ return donor
+
+
+def create_mode_of_payment(method):
+ frappe.get_doc({
+ 'doctype': 'Mode of Payment',
+ 'mode_of_payment': method
+ }).insert(ignore_mandatory=True)
+
+
+def notify_failure(log):
+ try:
+ content = '''
+ Dear System Manager,
+ Razorpay webhook for creating donation failed due to some reason.
+ Please check the error log linked below
+ Error Log: {0}
+ Regards, Administrator
+ '''.format(get_link_to_form('Error Log', log.name))
+
+ sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content)
+ except Exception:
+ pass
+
diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py
new file mode 100644
index 00000000000..7e25c8d2173
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'donation',
+ 'non_standard_fieldnames': {
+ 'Payment Entry': 'reference_name'
+ },
+ 'transactions': [
+ {
+ 'label': _('Payment'),
+ 'items': ['Payment Entry']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py
new file mode 100644
index 00000000000..c6a534dac34
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/test_donation.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.non_profit.doctype.donation.donation import create_donation
+
+class TestDonation(unittest.TestCase):
+ def setUp(self):
+ create_donor_type()
+ settings = frappe.get_doc('Non Profit Settings')
+ settings.company = '_Test Company'
+ settings.donation_company = '_Test Company'
+ settings.default_donor_type = '_Test Donor'
+ settings.automate_donation_payment_entries = 1
+ settings.donation_debit_account = 'Debtors - _TC'
+ settings.donation_payment_account = 'Cash - _TC'
+ settings.creation_user = 'Administrator'
+ settings.flags.ignore_permissions = True
+ settings.save()
+
+ def test_payment_entry_for_donations(self):
+ donor = create_donor()
+ create_mode_of_payment()
+ payment = frappe._dict({
+ 'amount': 100,
+ 'method': 'Debit Card',
+ 'id': 'pay_MeXAmsgeKOhq7O'
+ })
+ donation = create_donation(donor, payment)
+
+ self.assertTrue(donation.name)
+
+ # Naive test to check if at all payment entry is generated
+ # This method is actually triggered from Payment Gateway
+ # In any case if details were missing, this would throw an error
+ donation.on_payment_authorized()
+ donation.reload()
+
+ self.assertEquals(donation.paid, 1)
+ self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name}))
+
+
+def create_donor_type():
+ if not frappe.db.exists('Donor Type', '_Test Donor'):
+ frappe.get_doc({
+ 'doctype': 'Donor Type',
+ 'donor_type': '_Test Donor'
+ }).insert()
+
+
+def create_donor():
+ donor = frappe.db.exists('Donor', 'donor@test.com')
+ if donor:
+ return frappe.get_doc('Donor', 'donor@test.com')
+ else:
+ return frappe.get_doc({
+ 'doctype': 'Donor',
+ 'donor_name': '_Test Donor',
+ 'donor_type': '_Test Donor',
+ 'email': 'donor@test.com'
+ }).insert()
+
+
+def create_mode_of_payment():
+ if not frappe.db.exists('Mode of Payment', 'Debit Card'):
+ frappe.get_doc({
+ 'doctype': 'Mode of Payment',
+ 'mode_of_payment': 'Debit Card',
+ 'accounts': [{
+ 'company': '_Test Company',
+ 'default_account': 'Cash - _TC'
+ }]
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json
index 96392658f1a..72f24ef9226 100644
--- a/erpnext/non_profit/doctype/donor/donor.json
+++ b/erpnext/non_profit/doctype/donor/donor.json
@@ -76,8 +76,13 @@
}
],
"image_field": "image",
- "links": [],
- "modified": "2020-09-16 23:46:04.083274",
+ "links": [
+ {
+ "link_doctype": "Donation",
+ "link_fieldname": "donor"
+ }
+ ],
+ "modified": "2021-02-17 16:36:33.470731",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Donor",
diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py
index 9121d0cdfc8..fb70e59575b 100644
--- a/erpnext/non_profit/doctype/donor/donor.py
+++ b/erpnext/non_profit/doctype/donor/donor.py
@@ -11,3 +11,8 @@ class Donor(Document):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)
+ def validate(self):
+ from frappe.utils import validate_email_address
+ if self.email:
+ validate_email_address(self.email.strip(), True)
+
diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js
index 199dcfc04f5..6b8f1b1deb6 100644
--- a/erpnext/non_profit/doctype/member/member.js
+++ b/erpnext/non_profit/doctype/member/member.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('Member', {
setup: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false);
}
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 04b99f93f21..3ba2ee71c67 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.utils import cint
+from frappe.utils import cint, get_link_to_form
from frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
@@ -26,9 +26,10 @@ class Member(Document):
validate_email_address(email.strip(), True)
def setup_subscription(self):
- membership_settings = frappe.get_doc("Membership Settings")
- if not membership_settings.enable_razorpay:
- frappe.throw("Please enable Razorpay to setup subscription")
+ non_profit_settings = frappe.get_doc('Non Profit Settings')
+ if not non_profit_settings.enable_razorpay_for_memberships:
+ frappe.throw('Please check Enable Razorpay for Memberships in {0} to setup subscription').format(
+ get_link_to_form('Non Profit Settings', 'Non Profit Settings'))
controller = get_payment_gateway_controller("Razorpay")
settings = controller.get_settings({})
@@ -40,7 +41,7 @@ class Member(Document):
subscription_details = {
"plan_id": plan_id,
- "billing_frequency": cint(membership_settings.billing_frequency),
+ "billing_frequency": cint(non_profit_settings.billing_frequency),
"customer_notify": 1
}
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index 573ac3319a4..31872048a06 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => {
if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
})
},
@@ -26,7 +26,7 @@ frappe.ui.form.on('Membership', {
});
});
- frappe.db.get_single_value("Membership Settings", "send_email").then(val => {
+ frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => {
frm.reload_doc();
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 6da053f9fc4..11d32f9c2b4 100644
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ b/erpnext/non_profit/doctype/membership/membership.json
@@ -10,6 +10,7 @@
"member_name",
"membership_type",
"column_break_3",
+ "company",
"membership_status",
"membership_validity_section",
"from_date",
@@ -132,11 +133,18 @@
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-21 16:31:20.032656",
+ "modified": "2021-02-19 14:33:44.925122",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index c113b80d56f..191281f4cea 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import json
import frappe
import six
+import os
from datetime import datetime
from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers
@@ -58,7 +59,7 @@ class Membership(Document):
else:
self.from_date = nowdate()
- if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly":
+ if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
self.to_date = add_years(self.from_date, 1)
else:
self.to_date = add_months(self.from_date, 1)
@@ -68,9 +69,9 @@ class Membership(Document):
return
self.load_from_db()
self.db_set("paid", 1)
- settings = frappe.get_doc("Membership Settings")
- if settings.enable_invoicing and settings.create_for_web_forms:
- self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
+ settings = frappe.get_doc("Non Profit Settings")
+ if settings.allow_invoicing and settings.automate_membership_invoicing:
+ self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
def generate_invoice(self, save=True, with_payment_entry=False):
@@ -85,7 +86,7 @@ class Membership(Document):
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type)
- settings = frappe.get_doc("Membership Settings")
+ settings = frappe.get_doc("Non Profit Settings")
self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
@@ -102,7 +103,7 @@ class Membership(Document):
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
- if not settings.debit_account:
+ if not settings.membership_debit_account:
frappe.throw(_("You need to set
Debit Account in {0}").format(settings_link))
if not settings.company:
@@ -113,25 +114,26 @@ class Membership(Document):
get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice):
- if not settings.payment_account:
- frappe.throw(_("You need to set
Payment Account in {0}").format(
- get_link_to_form("Membership Type", self.membership_type)))
+ if not settings.membership_payment_account:
+ frappe.throw(_("You need to set
Payment Account for Membership in {0}").format(
+ get_link_to_form("Non Profit Settings", "Non Profit Settings")))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False
- pe.paid_to = settings.payment_account
+ pe.paid_to = settings.membership_payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
- pe.save(ignore_permissions=True)
+ pe.flags.ignore_mandatory = True
+ pe.save()
pe.submit()
def send_acknowlement(self):
- settings = frappe.get_doc("Membership Settings")
+ settings = frappe.get_doc("Non Profit Settings")
if not settings.send_email:
frappe.throw(_("You need to enable
Send Acknowledge Email in {0}").format(
- get_link_to_form("Membership Settings", "Membership Settings")))
+ get_link_to_form("Non Profit Settings", "Non Profit Settings")))
member = frappe.get_doc("Member", self.member)
if not member.email_id:
@@ -170,7 +172,7 @@ def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
"doctype": "Sales Invoice",
"customer": member.customer,
- "debit_to": settings.debit_account,
+ "debit_to": settings.membership_debit_account,
"currency": membership.currency,
"company": settings.company,
"is_pos": 0,
@@ -183,7 +185,7 @@ def make_invoice(membership, member, plan, settings):
]
})
invoice.set_missing_values()
- invoice.insert(ignore_permissions=True)
+ invoice.insert()
invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
@@ -203,17 +205,18 @@ def get_member_based_on_subscription(subscription_id, email):
return None
-def verify_signature(data):
- if frappe.flags.in_test:
+def verify_signature(data, endpoint="Membership"):
+ if frappe.flags.in_test or os.environ.get("CI"):
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
- settings = frappe.get_doc("Membership Settings")
- key = settings.get_webhook_secret()
+ settings = frappe.get_doc("Non Profit Settings")
+ key = settings.get_webhook_secret(endpoint)
controller = frappe.get_doc("Razorpay Settings")
controller.verify_signature(data, signature, key)
+ frappe.set_user(settings.creation_user)
@frappe.whitelist(allow_guest=True)
@@ -222,7 +225,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
try:
verify_signature(data)
except Exception as e:
- log = frappe.log_error(e, "Webhook Verification Error")
+ log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return { "status": "Failed", "reason": e}
@@ -250,16 +253,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
- if subscription.notes and type(subscription.notes) == dict:
- notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
- member.add_comment("Comment", notes)
- elif subscription.notes and type(subscription.notes) == str:
- member.add_comment("Comment", subscription.notes)
+ if subscription.get("notes"):
+ member = get_additional_notes(member, subscription)
+ company = get_company_for_memberships()
# Update Membership
membership = frappe.new_doc("Membership")
membership.update({
+ "company": company,
"member": member.name,
"membership_status": "Current",
"membership_type": member.membership_type,
@@ -270,13 +272,20 @@ def trigger_razorpay_subscription(*args, **kwargs):
"to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise
})
- membership.insert(ignore_permissions=True)
+ membership.flags.ignore_mandatory = True
+ membership.insert()
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1
- member.save(ignore_permissions=True)
+ member.flags.ignore_mandatory = True
+ member.save()
+
+ settings = frappe.get_doc("Non Profit Settings")
+ if settings.allow_invoicing and settings.automate_membership_invoicing:
+ membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
+
except Exception as e:
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
@@ -286,6 +295,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
return { "status": "Success" }
+def get_company_for_memberships():
+ company = frappe.db.get_single_value("Non Profit Settings", "company")
+ if not company:
+ from erpnext.healthcare.setup import get_company
+ company = get_company()
+ return company
+
+
+def get_additional_notes(member, subscription):
+ if type(subscription.notes) == dict:
+ for k, v in subscription.notes.items():
+ notes = "\n".join("{}: {}".format(k, v))
+
+ # extract member name from notes
+ if "name" in k.lower():
+ member.update({
+ "member_name": subscription.notes.get(k)
+ })
+
+ # extract pan number from notes
+ if "pan" in k.lower():
+ member.update({
+ "pan_number": subscription.notes.get(k)
+ })
+
+ member.add_comment("Comment", notes)
+
+ elif type(subscription.notes) == str:
+ member.add_comment("Comment", subscription.notes)
+
+ return member
+
+
def notify_failure(log):
try:
content = """
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index ff7e6c473c5..31da792e534 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
def setUp(self):
- # Get default company
- company = frappe.get_doc("Company", erpnext.get_default_company())
-
- # update membership settings
- settings = frappe.get_doc("Membership Settings")
- # Enable razorpay
- settings.enable_razorpay = 1
- settings.billing_cycle = "Monthly"
- settings.billing_frequency = 24
- # Enable invoicing
- settings.enable_invoicing = 1
- settings.make_payment_entry = 1
- settings.company = company.name
- settings.payment_account = company.default_cash_account
- settings.debit_account = company.default_receivable_account
- settings.save()
-
- # make test plan
- if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
- plan = frappe.new_doc("Membership Type")
- plan.membership_type = "_rzpy_test_milythm"
- plan.amount = 100
- plan.razorpay_plan_id = "_rzpy_test_milythm"
- plan.linked_item = create_item("_Test Item for Non Profit Membership").name
- plan.insert()
- else:
- plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+ plan = setup_membership()
# make test member
self.member_doc = create_member(frappe._dict({
@@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase):
})
def set_config(key, value):
- frappe.db.set_value("Membership Settings", None, key, value)
+ frappe.db.set_value("Non Profit Settings", None, key, value)
def make_membership(member, payload={}):
data = {
@@ -109,3 +83,36 @@ def create_item(item_code):
else:
item = frappe.get_doc("Item", item_code)
return item
+
+def setup_membership():
+ # Get default company
+ company = frappe.get_doc("Company", erpnext.get_default_company())
+
+ # update non profit settings
+ settings = frappe.get_doc("Non Profit Settings")
+ # Enable razorpay
+ settings.enable_razorpay_for_memberships = 1
+ settings.billing_cycle = "Monthly"
+ settings.billing_frequency = 24
+ # Enable invoicing
+ settings.allow_invoicing = 1
+ settings.automate_membership_payment_entries = 1
+ settings.company = company.name
+ settings.donation_company = company.name
+ settings.membership_payment_account = company.default_cash_account
+ settings.membership_debit_account = company.default_receivable_account
+ settings.flags.ignore_mandatory = True
+ settings.save()
+
+ # make test plan
+ if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
+ plan = frappe.new_doc("Membership Type")
+ plan.membership_type = "_rzpy_test_milythm"
+ plan.amount = 100
+ plan.razorpay_plan_id = "_rzpy_test_milythm"
+ plan.linked_item = create_item("_Test Item for Non Profit Membership").name
+ plan.insert()
+ else:
+ plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+
+ return plan
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
deleted file mode 100644
index 3887b0a2bea..00000000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json
+++ /dev/null
@@ -1,192 +0,0 @@
-{
- "actions": [],
- "creation": "2020-03-29 12:57:03.005120",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enable_razorpay",
- "razorpay_settings_section",
- "billing_cycle",
- "billing_frequency",
- "webhook_secret",
- "column_break_6",
- "enable_invoicing",
- "create_for_web_forms",
- "make_payment_entry",
- "company",
- "debit_account",
- "payment_account",
- "column_break_9",
- "send_email",
- "send_invoice",
- "membership_print_format",
- "inv_print_format",
- "email_template"
- ],
- "fields": [
- {
- "fieldname": "billing_cycle",
- "fieldtype": "Select",
- "label": "Billing Cycle",
- "options": "Monthly\nYearly"
- },
- {
- "default": "0",
- "fieldname": "enable_razorpay",
- "fieldtype": "Check",
- "label": "Enable RazorPay For Memberships"
- },
- {
- "depends_on": "eval:doc.enable_razorpay",
- "fieldname": "razorpay_settings_section",
- "fieldtype": "Section Break",
- "label": "RazorPay Settings"
- },
- {
- "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
- "fieldname": "billing_frequency",
- "fieldtype": "Int",
- "label": "Billing Frequency"
- },
- {
- "fieldname": "webhook_secret",
- "fieldtype": "Password",
- "label": "Webhook Secret",
- "read_only": 1
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Section Break",
- "label": "Invoicing"
- },
- {
- "depends_on": "eval:doc.enable_invoicing",
- "fieldname": "debit_account",
- "fieldtype": "Link",
- "label": "Debit Account",
- "mandatory_depends_on": "eval:doc.enable_auto_invoicing",
- "options": "Account"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:doc.enable_invoicing",
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "mandatory_depends_on": "eval:doc.enable_auto_invoicing",
- "options": "Company"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing && doc.send_email",
- "fieldname": "send_invoice",
- "fieldtype": "Check",
- "label": "Send Invoice with Email"
- },
- {
- "default": "0",
- "fieldname": "send_email",
- "fieldtype": "Check",
- "label": "Send Membership Acknowledgement"
- },
- {
- "depends_on": "eval: doc.send_invoice",
- "fieldname": "inv_print_format",
- "fieldtype": "Link",
- "label": "Invoice Print Format",
- "mandatory_depends_on": "eval: doc.send_invoice",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "membership_print_format",
- "fieldtype": "Link",
- "label": "Membership Print Format",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "email_template",
- "fieldtype": "Link",
- "label": "Email Template",
- "mandatory_depends_on": "eval:doc.send_email",
- "options": "Email Template"
- },
- {
- "default": "0",
- "fieldname": "enable_invoicing",
- "fieldtype": "Check",
- "label": "Enable Invoicing",
- "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing",
- "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
- "fieldname": "make_payment_entry",
- "fieldtype": "Check",
- "label": "Make Payment Entry"
- },
- {
- "depends_on": "eval:doc.make_payment_entry",
- "fieldname": "payment_account",
- "fieldtype": "Link",
- "label": "Payment To",
- "mandatory_depends_on": "eval:doc.make_payment_entry",
- "options": "Account"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing",
- "description": "Automatically create an invoice when payment is authorized from a web form entry",
- "fieldname": "create_for_web_forms",
- "fieldtype": "Check",
- "label": "Auto Create Invoice for Web Forms"
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2021-01-21 19:57:53.213286",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- },
- {
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Member",
- "share": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py
deleted file mode 100644
index f3b2eee6f97..00000000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py
+++ /dev/null
@@ -1,33 +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 _
-from frappe.integrations.utils import get_payment_gateway_controller
-from frappe.model.document import Document
-
-class MembershipSettings(Document):
- def generate_webhook_key(self):
- key = frappe.generate_hash(length=20)
- self.webhook_secret = key
- self.save()
-
- frappe.msgprint(
- _("Here is your webhook secret, this will be shown to you only once.") + "
" + key,
- _("Webhook Secret")
- );
-
- def revoke_key(self):
- self.webhook_secret = None;
- self.save()
-
- def get_webhook_secret(self):
- return self.get_password(fieldname="webhook_secret", raise_exception=False)
-
-@frappe.whitelist()
-def get_plans_for_membership(*args, **kwargs):
- controller = get_payment_gateway_controller("Razorpay")
- plans = controller.get_plans()
- return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 91a5cb74ba1..2f2427629c3 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.js
@@ -3,11 +3,11 @@
frappe.ui.form.on('Membership Type', {
refresh: function (frm) {
- frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
});
- frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py
rename to erpnext/non_profit/doctype/non_profit_settings/__init__.py
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
similarity index 50%
rename from erpnext/non_profit/doctype/membership_settings/membership_settings.js
rename to erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
index c95aab2a7a1..cff92b42abb 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
@@ -1,16 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on("Membership Settings", {
+frappe.ui.form.on("Non Profit Settings", {
refresh: function(frm) {
- if (frm.doc.webhook_secret) {
- frm.add_custom_button(__("Revoke
"), () => {
- frm.call("revoke_key").then(() => {
- frm.refresh();
- })
- });
- }
-
frm.set_query("inv_print_format", function() {
return {
filters: {
@@ -37,7 +29,7 @@ frappe.ui.form.on("Membership Settings", {
};
});
- frm.set_query("payment_account", function () {
+ frm.set_query("membership_payment_account", function () {
var account_types = ["Bank", "Cash"];
return {
filters: {
@@ -51,31 +43,70 @@ frappe.ui.form.on("Membership Settings", {
let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
frm.set_intro(__("You can learn more about memberships in the manual. ") + `
${__('ERPNext Docs')} `, true);
-
- frm.trigger("add_generate_button");
- frm.trigger("add_copy_buttonn");
+ frm.trigger("setup_buttons_for_membership");
+ frm.trigger("setup_buttons_for_donation");
},
- add_generate_button: function(frm) {
+ setup_buttons_for_membership: function(frm) {
let label;
- if (frm.doc.webhook_secret) {
+ if (frm.doc.membership_webhook_secret) {
+
+ frm.add_custom_button(__("Copy Webhook URL"), () => {
+ frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
+ }, __("Memberships"));
+
+ frm.add_custom_button(__("Revoke Key"), () => {
+ frm.call("revoke_key", {
+ key: "membership_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Memberships"));
+
label = __("Regenerate Webhook Secret");
+
} else {
label = __("Generate Webhook Secret");
}
+
frm.add_custom_button(label, () => {
- frm.call("generate_webhook_key").then(() => {
+ frm.call("generate_webhook_secret", {
+ field: "membership_webhook_secret"
+ }).then(() => {
frm.refresh();
});
- });
+ }, __("Memberships"));
},
- add_copy_buttonn: function(frm) {
- if (frm.doc.webhook_secret) {
+ setup_buttons_for_donation: function(frm) {
+ let label;
+
+ if (frm.doc.donation_webhook_secret) {
+ label = __("Regenerate Webhook Secret");
+
frm.add_custom_button(__("Copy Webhook URL"), () => {
- frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
- });
+ frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`);
+ }, __("Donations"));
+
+ frm.add_custom_button(__("Revoke Key"), () => {
+ frm.call("revoke_key", {
+ key: "donation_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Donations"));
+
+ } else {
+ label = __("Generate Webhook Secret");
}
+
+ frm.add_custom_button(label, () => {
+ frm.call("generate_webhook_secret", {
+ field: "donation_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Donations"));
}
});
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
new file mode 100644
index 00000000000..25ff0c1bb02
--- /dev/null
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
@@ -0,0 +1,273 @@
+{
+ "actions": [],
+ "creation": "2020-03-29 12:57:03.005120",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable_razorpay_for_memberships",
+ "razorpay_settings_section",
+ "billing_cycle",
+ "billing_frequency",
+ "membership_webhook_secret",
+ "column_break_6",
+ "allow_invoicing",
+ "automate_membership_invoicing",
+ "automate_membership_payment_entries",
+ "company",
+ "membership_debit_account",
+ "membership_payment_account",
+ "column_break_9",
+ "send_email",
+ "send_invoice",
+ "membership_print_format",
+ "inv_print_format",
+ "email_template",
+ "donation_settings_section",
+ "donation_company",
+ "default_donor_type",
+ "donation_webhook_secret",
+ "column_break_22",
+ "automate_donation_payment_entries",
+ "donation_debit_account",
+ "donation_payment_account",
+ "section_break_27",
+ "creation_user"
+ ],
+ "fields": [
+ {
+ "fieldname": "billing_cycle",
+ "fieldtype": "Select",
+ "label": "Billing Cycle",
+ "options": "Monthly\nYearly"
+ },
+ {
+ "depends_on": "eval:doc.enable_razorpay_for_memberships",
+ "fieldname": "razorpay_settings_section",
+ "fieldtype": "Section Break",
+ "label": "RazorPay Settings for Memberships"
+ },
+ {
+ "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
+ "fieldname": "billing_frequency",
+ "fieldtype": "Int",
+ "label": "Billing Frequency"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Section Break",
+ "label": "Membership Invoicing"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "This company will be set for the Memberships created via webhook.",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.allow_invoicing && doc.send_email",
+ "fieldname": "send_invoice",
+ "fieldtype": "Check",
+ "label": "Send Invoice with Email"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_email",
+ "fieldtype": "Check",
+ "label": "Send Membership Acknowledgement"
+ },
+ {
+ "depends_on": "eval: doc.send_invoice",
+ "fieldname": "inv_print_format",
+ "fieldtype": "Link",
+ "label": "Invoice Print Format",
+ "mandatory_depends_on": "eval: doc.send_invoice",
+ "options": "Print Format"
+ },
+ {
+ "depends_on": "eval:doc.send_email",
+ "fieldname": "membership_print_format",
+ "fieldtype": "Link",
+ "label": "Membership Print Format",
+ "options": "Print Format"
+ },
+ {
+ "depends_on": "eval:doc.send_email",
+ "fieldname": "email_template",
+ "fieldtype": "Link",
+ "label": "Email Template",
+ "mandatory_depends_on": "eval:doc.send_email",
+ "options": "Email Template"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_invoicing",
+ "fieldtype": "Check",
+ "label": "Allow Invoicing for Memberships",
+ "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.allow_invoicing",
+ "description": "Automatically create an invoice when payment is authorized from a web form entry",
+ "fieldname": "automate_membership_invoicing",
+ "fieldtype": "Check",
+ "label": "Automate Invoicing for Web Forms"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.allow_invoicing",
+ "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
+ "fieldname": "automate_membership_payment_entries",
+ "fieldtype": "Check",
+ "label": "Automate Payment Entry Creation"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_razorpay_for_memberships",
+ "fieldtype": "Check",
+ "label": "Enable RazorPay For Memberships"
+ },
+ {
+ "depends_on": "eval:doc.automate_membership_payment_entries",
+ "description": "Account for accepting membership payments",
+ "fieldname": "membership_payment_account",
+ "fieldtype": "Link",
+ "label": "Membership Payment To",
+ "mandatory_depends_on": "eval:doc.automate_membership_payment_entries",
+ "options": "Account"
+ },
+ {
+ "fieldname": "membership_webhook_secret",
+ "fieldtype": "Password",
+ "label": "Membership Webhook Secret",
+ "read_only": 1
+ },
+ {
+ "fieldname": "donation_webhook_secret",
+ "fieldtype": "Password",
+ "label": "Donation Webhook Secret",
+ "read_only": 1
+ },
+ {
+ "depends_on": "automate_donation_payment_entries",
+ "description": "Account for accepting donation payments",
+ "fieldname": "donation_payment_account",
+ "fieldtype": "Link",
+ "label": "Donation Payment To",
+ "mandatory_depends_on": "automate_donation_payment_entries",
+ "options": "Account"
+ },
+ {
+ "default": "0",
+ "description": "Auto creates Payment Entry for Donations created from web forms.",
+ "fieldname": "automate_donation_payment_entries",
+ "fieldtype": "Check",
+ "label": "Automate Donation Payment Entries"
+ },
+ {
+ "depends_on": "eval:doc.allow_invoicing",
+ "fieldname": "membership_debit_account",
+ "fieldtype": "Link",
+ "label": "Debit Account",
+ "mandatory_depends_on": "eval:doc.allow_invoicing",
+ "options": "Account"
+ },
+ {
+ "depends_on": "automate_donation_payment_entries",
+ "fieldname": "donation_debit_account",
+ "fieldtype": "Link",
+ "label": "Debit Account",
+ "mandatory_depends_on": "automate_donation_payment_entries",
+ "options": "Account"
+ },
+ {
+ "description": "This company will be set for the Donations created via webhook.",
+ "fieldname": "donation_company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "donation_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Donation Settings"
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "This Donor Type will be set for the Donor created via Donation web form entry.",
+ "fieldname": "default_donor_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Default Donor Type",
+ "options": "Donor Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_27",
+ "fieldtype": "Section Break"
+ },
+ {
+ "description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.",
+ "fieldname": "creation_user",
+ "fieldtype": "Link",
+ "label": "Creation User",
+ "options": "User",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-03-11 10:43:38.124240",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Non Profit Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Non Profit Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Non Profit Member",
+ "share": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
new file mode 100644
index 00000000000..108554c6a08
--- /dev/null
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
@@ -0,0 +1,36 @@
+# -*- 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 _
+from frappe.integrations.utils import get_payment_gateway_controller
+from frappe.model.document import Document
+
+class NonProfitSettings(Document):
+ def generate_webhook_secret(self, field="membership_webhook_secret"):
+ key = frappe.generate_hash(length=20)
+ self.set(field, key)
+ self.save()
+
+ secret_for = "Membership" if field == "membership_webhook_secret" else "Donation"
+
+ frappe.msgprint(
+ _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "
" + key,
+ _("Webhook Secret")
+ )
+
+ def revoke_key(self, key):
+ self.set(key, None)
+ self.save()
+
+ def get_webhook_secret(self, endpoint="Membership"):
+ fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret"
+ return self.get_password(fieldname=fieldname, raise_exception=False)
+
+@frappe.whitelist()
+def get_plans_for_membership(*args, **kwargs):
+ controller = get_payment_gateway_controller("Razorpay")
+ plans = controller.get_plans()
+ return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
similarity index 79%
rename from erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
rename to erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
index 2ad7984583d..3f0ede32e59 100644
--- a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
+++ b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
-class TestMembershipSettings(unittest.TestCase):
+class TestNonProfitSettings(unittest.TestCase):
pass
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
index da2a514810b..2557d77d881 100644
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ b/erpnext/non_profit/workspace/non_profit/non_profit.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "non-profit",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Non Profit",
"links": [
@@ -109,7 +110,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Membership Settings",
- "link_to": "Membership Settings",
+ "link_to": "Non Profit Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -161,7 +162,7 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Donor",
+ "label": "Donation",
"onboard": 0,
"type": "Card Break"
},
@@ -184,9 +185,35 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donation",
+ "link_to": "Donation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Exemption Certification (India)",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Exemption 80G Certificate",
+ "link_to": "Tax Exemption 80G Certificate",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2020-12-01 13:38:38.351409",
+ "modified": "2021-03-11 11:38:09.140655",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
@@ -201,8 +228,8 @@
"type": "DocType"
},
{
- "label": "Membership Settings",
- "link_to": "Membership Settings",
+ "label": "Non Profit Settings",
+ "link_to": "Non Profit Settings",
"type": "DocType"
},
{
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index e5ee551c118..59b12f319eb 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -754,3 +754,8 @@ erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes
erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
+erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
+erpnext.patches.v13_0.update_vehicle_no_reqd_condition
+erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
+erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
+erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py
index 5dc5d3bf0cf..b997ba2db22 100644
--- a/erpnext/patches/v11_0/refactor_autoname_naming.py
+++ b/erpnext/patches/v11_0/refactor_autoname_naming.py
@@ -20,7 +20,7 @@ doctype_series_map = {
'Certified Consultant': 'NPO-CONS-.YYYY.-.#####',
'Chat Room': 'CHAT-ROOM-.#####',
'Compensatory Leave Request': 'HR-CMP-.YY.-.MM.-.#####',
- 'Custom Script': 'SYS-SCR-.#####',
+ 'Client Script': 'SYS-SCR-.#####',
'Employee Benefit Application': 'HR-BEN-APP-.YY.-.MM.-.#####',
'Employee Benefit Application Detail': '',
'Employee Benefit Claim': 'HR-BEN-CLM-.YY.-.MM.-.#####',
diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py
index 1acdfcccf9f..544bc5e6911 100644
--- a/erpnext/patches/v11_1/update_bank_transaction_status.py
+++ b/erpnext/patches/v11_1/update_bank_transaction_status.py
@@ -7,9 +7,20 @@ import frappe
def execute():
frappe.reload_doc("accounts", "doctype", "bank_transaction")
- frappe.db.sql(""" UPDATE `tabBank Transaction`
- SET status = 'Reconciled'
- WHERE
- status = 'Settled' and (debit = allocated_amount or credit = allocated_amount)
- and ifnull(allocated_amount, 0) > 0
- """)
\ No newline at end of file
+ bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns()
+
+ if 'debit' in bank_transaction_fields:
+ frappe.db.sql(""" UPDATE `tabBank Transaction`
+ SET status = 'Reconciled'
+ WHERE
+ status = 'Settled' and (debit = allocated_amount or credit = allocated_amount)
+ and ifnull(allocated_amount, 0) > 0
+ """)
+
+ elif 'deposit' in bank_transaction_fields:
+ frappe.db.sql(""" UPDATE `tabBank Transaction`
+ SET status = 'Reconciled'
+ WHERE
+ status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount)
+ and ifnull(allocated_amount, 0) > 0
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py
new file mode 100644
index 00000000000..af1f6e7ec17
--- /dev/null
+++ b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ doctypes = [
+ "Bank Statement Settings",
+ "Bank Statement Settings Item",
+ "Bank Statement Transaction Entry",
+ "Bank Statement Transaction Invoice Item",
+ "Bank Statement Transaction Payment Item",
+ "Bank Statement Transaction Settings Item",
+ "Bank Statement Transaction Settings",
+ ]
+
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, force=1)
+
+ frappe.delete_doc("Page", "bank-reconciliation", force=1)
+
+ rename_field("Bank Transaction", "debit", "deposit")
+ rename_field("Bank Transaction", "credit", "withdrawal")
diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
index f60e0d3036c..d968e1fb763 100644
--- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
+++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
@@ -1,14 +1,41 @@
import frappe
from frappe import _
+from frappe.utils import getdate, get_time, today
from erpnext.stock.stock_ledger import update_entries_after
from erpnext.accounts.utils import update_gl_entries_after
def execute():
- data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
- from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0
- order by timestamp(posting_date, posting_time) asc, creation asc''', as_dict=1)
+ for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item',
+ 'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'):
+ frappe.reload_doc('stock', 'doctype', doctype)
+ frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied')
- for index, d in enumerate(data):
+ reposting_project_deployed_on = get_creation_time()
+ posting_date = getdate(reposting_project_deployed_on)
+ posting_time = get_time(reposting_project_deployed_on)
+
+ if posting_date == today():
+ return
+
+ frappe.clear_cache()
+ frappe.flags.warehouse_account_map = {}
+
+ data = frappe.db.sql('''
+ SELECT
+ name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
+ FROM
+ `tabStock Ledger Entry`
+ WHERE
+ creation > %s
+ and is_cancelled = 0
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ ''', reposting_project_deployed_on, as_dict=1)
+
+ frappe.db.auto_commit_on_many_writes = 1
+ print("Reposting Stock Ledger Entries...")
+ total_sle = len(data)
+ i = 0
+ for d in data:
update_entries_after({
"item_code": d.item_code,
"warehouse": d.warehouse,
@@ -19,9 +46,18 @@ def execute():
"sle_id": d.name
}, allow_negative_stock=True)
- frappe.db.auto_commit_on_many_writes = 1
+ i += 1
+ if i%100 == 0:
+ print(i, "/", total_sle)
+
+
+ print("Reposting General Ledger Entries...")
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
- update_gl_entries_after('2020-12-25', '01:58:55', company=row.name)
+ update_gl_entries_after(posting_date, posting_time, company=row.name)
- frappe.db.auto_commit_on_many_writes = 0
\ No newline at end of file
+ frappe.db.auto_commit_on_many_writes = 0
+
+def get_creation_time():
+ return frappe.db.sql(''' SELECT create_time FROM
+ INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0]
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py
new file mode 100644
index 00000000000..3fa09a7baaa
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py
@@ -0,0 +1,22 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ if frappe.db.table_exists("Membership Settings"):
+ frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings")
+ frappe.reload_doctype("Non Profit Settings", force=True)
+
+ if frappe.db.table_exists("Non Profit Settings"):
+ rename_fields_map = {
+ "enable_invoicing": "allow_invoicing",
+ "create_for_web_forms": "automate_membership_invoicing",
+ "make_payment_entry": "automate_membership_payment_entries",
+ "enable_razorpay": "enable_razorpay_for_memberships",
+ "debit_account": "membership_debit_account",
+ "payment_account": "membership_payment_account",
+ "webhook_secret": "membership_webhook_secret"
+ }
+
+ for old_name, new_name in rename_fields_map.items():
+ rename_field("Non Profit Settings", old_name, new_name)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
new file mode 100644
index 00000000000..aea53f8adda
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
@@ -0,0 +1,16 @@
+import frappe
+from erpnext.regional.india.setup import make_custom_fields
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ make_custom_fields()
+
+ if not frappe.db.exists('Party Type', 'Donor'):
+ frappe.get_doc({
+ 'doctype': 'Party Type',
+ 'party_type': 'Donor',
+ 'account_type': 'Receivable'
+ }).insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py
new file mode 100644
index 00000000000..01fd6a158e9
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('payroll', 'doctype', 'gratuity_rule')
+ frappe.reload_doc('payroll', 'doctype', 'gratuity_rule_slab')
+ frappe.reload_doc('payroll', 'doctype', 'gratuity_applicable_component')
+ if frappe.db.exists("Company", {"country": "India"}):
+ from erpnext.regional.india.setup import create_gratuity_rule
+ create_gratuity_rule()
+ if frappe.db.exists("Company", {"country": "United Arab Emirates"}):
+ from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
+ create_gratuity_rule()
diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
new file mode 100644
index 00000000000..c26cddbe4e5
--- /dev/null
+++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }):
+ frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '')
diff --git a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
index ef3f1d6c0a0..c564f8b02ab 100644
--- a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
+++ b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
@@ -9,7 +9,7 @@ def execute():
# NOTE: sequence is important
renamed_fields = get_all_renamed_fields()
- for dt, script_field, ref_dt_field in (("Custom Script", "script", "dt"), ("Print Format", "html", "doc_type")):
+ for dt, script_field, ref_dt_field in (("Client Script", "script", "dt"), ("Print Format", "html", "doc_type")):
cond1 = " or ".join("""{0} like "%%{1}%%" """.format(script_field, d[0].replace("_", "\\_")) for d in renamed_fields)
cond2 = " and standard = 'No'" if dt == "Print Format" else ""
diff --git a/erpnext/patches/v7_0/remove_doctypes_and_reports.py b/erpnext/patches/v7_0/remove_doctypes_and_reports.py
index 746cae0e1ca..2356e2f6ee4 100644
--- a/erpnext/patches/v7_0/remove_doctypes_and_reports.py
+++ b/erpnext/patches/v7_0/remove_doctypes_and_reports.py
@@ -7,7 +7,7 @@ def execute():
where name in('Time Log Batch', 'Time Log Batch Detail', 'Time Log')""")
frappe.db.sql("""delete from `tabDocField` where parent in ('Time Log', 'Time Log Batch')""")
- frappe.db.sql("""update `tabCustom Script` set dt = 'Timesheet' where dt = 'Time Log'""")
+ frappe.db.sql("""update `tabClient Script` set dt = 'Timesheet' where dt = 'Time Log'""")
for data in frappe.db.sql(""" select label, fieldname from `tabCustom Field` where dt = 'Time Log'""", as_dict=1):
custom_field = frappe.get_doc({
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/payroll/doctype/gratuity/__init__.py
similarity index 100%
rename from erpnext/accounts/page/bank_reconciliation/__init__.py
rename to erpnext/payroll/doctype/gratuity/__init__.py
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js
new file mode 100644
index 00000000000..565d2c49f94
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.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('Gratuity', {
+ setup: function (frm) {
+ frm.set_query('salary_component', function () {
+ return {
+ filters: {
+ type: "Earning"
+ }
+ };
+ });
+ frm.set_query("expense_account", function () {
+ return {
+ filters: {
+ "root_type": "Expense",
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+
+ frm.set_query("payable_account", function () {
+ return {
+ filters: {
+ "root_type": "Liability",
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+ },
+ refresh: function (frm) {
+ if (frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") {
+ frm.add_custom_button(__("Create Payment Entry"), function () {
+ return frappe.call({
+ method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
+ args: {
+ "dt": frm.doc.doctype,
+ "dn": frm.doc.name
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ });
+ }
+ },
+ employee: function (frm) {
+ frm.events.calculate_work_experience_and_amount(frm);
+ },
+ gratuity_rule: function (frm) {
+ frm.events.calculate_work_experience_and_amount(frm);
+ },
+ calculate_work_experience_and_amount: function (frm) {
+
+ if (frm.doc.employee && frm.doc.gratuity_rule) {
+ frappe.call({
+ method: "erpnext.payroll.doctype.gratuity.gratuity.calculate_work_experience_and_amount",
+ args: {
+ employee: frm.doc.employee,
+ gratuity_rule: frm.doc.gratuity_rule
+ }
+ }).then((r) => {
+ frm.set_value("current_work_experience", r.message['current_work_experience']);
+ frm.set_value("amount", r.message['amount']);
+ });
+ }
+ }
+
+});
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json
new file mode 100644
index 00000000000..5cffd7eebf9
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.json
@@ -0,0 +1,232 @@
+{
+ "actions": [],
+ "autoname": "HR-GRA-PAY-.#####",
+ "creation": "2020-08-05 20:52:13.024683",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "department",
+ "designation",
+ "column_break_3",
+ "posting_date",
+ "status",
+ "company",
+ "gratuity_rule",
+ "section_break_5",
+ "pay_via_salary_slip",
+ "payroll_date",
+ "salary_component",
+ "payable_account",
+ "expense_account",
+ "mode_of_payment",
+ "cost_center",
+ "column_break_15",
+ "current_work_experience",
+ "amount",
+ "paid_amount",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "pay_via_salary_slip",
+ "fieldtype": "Check",
+ "label": "Pay via Salary Slip"
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting date",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 1",
+ "fieldname": "salary_component",
+ "fieldtype": "Link",
+ "label": "Salary Component",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1",
+ "options": "Salary Component"
+ },
+ {
+ "default": "0",
+ "fieldname": "current_work_experience",
+ "fieldtype": "Int",
+ "label": "Current Work Experience",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Total Amount",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Draft\nUnpaid\nPaid",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "expense_account",
+ "fieldtype": "Link",
+ "label": "Expense Account",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "gratuity_rule",
+ "fieldtype": "Link",
+ "label": "Gratuity Rule",
+ "options": "Gratuity Rule",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Payment Configuration"
+ },
+ {
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.designation",
+ "fieldname": "designation",
+ "fieldtype": "Data",
+ "label": "Designation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Gratuity",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 1",
+ "fieldname": "payroll_date",
+ "fieldtype": "Date",
+ "label": "Payroll Date",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.pay_via_salary_slip == 0",
+ "fieldname": "paid_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Amount",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "payable_account",
+ "fieldtype": "Link",
+ "label": "Payable Account",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Cost Center"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-11-02 18:21:11.971488",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity",
+ "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
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py
new file mode 100644
index 00000000000..1acd6e342fd
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.py
@@ -0,0 +1,249 @@
+# -*- 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 _, bold
+from frappe.utils import flt, get_datetime, get_link_to_form
+from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.controllers.accounts_controller import AccountsController
+from math import floor
+
+class Gratuity(AccountsController):
+ def validate(self):
+ data = calculate_work_experience_and_amount(self.employee, self.gratuity_rule)
+ self.current_work_experience = data["current_work_experience"]
+ self.amount = data["amount"]
+ if self.docstatus == 1:
+ self.status = "Unpaid"
+
+ def on_submit(self):
+ if self.pay_via_salary_slip:
+ self.create_additional_salary()
+ else:
+ self.create_gl_entries()
+
+ def on_cancel(self):
+ self.ignore_linked_doctypes = ['GL Entry']
+ self.create_gl_entries(cancel=True)
+
+ def create_gl_entries(self, cancel=False):
+ gl_entries = self.get_gl_entries()
+ make_gl_entries(gl_entries, cancel)
+
+ def get_gl_entries(self):
+ gl_entry = []
+ # payable entry
+ if self.amount:
+ gl_entry.append(
+ self.get_gl_dict({
+ "account": self.payable_account,
+ "credit": self.amount,
+ "credit_in_account_currency": self.amount,
+ "against": self.expense_account,
+ "party_type": "Employee",
+ "party": self.employee,
+ "against_voucher_type": self.doctype,
+ "against_voucher": self.name,
+ "cost_center": self.cost_center
+ }, item=self)
+ )
+
+ # expense entries
+ gl_entry.append(
+ self.get_gl_dict({
+ "account": self.expense_account,
+ "debit": self.amount,
+ "debit_in_account_currency": self.amount,
+ "against": self.payable_account,
+ "cost_center": self.cost_center
+ }, item=self)
+ )
+ else:
+ frappe.throw(_("Total Amount can not be zero"))
+
+ return gl_entry
+
+ def create_additional_salary(self):
+ if self.pay_via_salary_slip:
+ additional_salary = frappe.new_doc('Additional Salary')
+ additional_salary.employee = self.employee
+ additional_salary.salary_component = self.salary_component
+ additional_salary.overwrite_salary_structure_amount = 0
+ additional_salary.amount = self.amount
+ additional_salary.payroll_date = self.payroll_date
+ additional_salary.company = self.company
+ additional_salary.ref_doctype = self.doctype
+ additional_salary.ref_docname = self.name
+ additional_salary.submit()
+
+ def set_total_advance_paid(self):
+ paid_amount = frappe.db.sql("""
+ select ifnull(sum(debit_in_account_currency), 0) as paid_amount
+ from `tabGL Entry`
+ where against_voucher_type = 'Gratuity'
+ and against_voucher = %s
+ and party_type = 'Employee'
+ and party = %s
+ """, (self.name, self.employee), as_dict=1)[0].paid_amount
+
+ if flt(paid_amount) > self.amount:
+ frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount"))
+
+
+ self.db_set("paid_amount", paid_amount)
+ if self.amount == self.paid_amount:
+ self.db_set("status", "Paid")
+
+
+@frappe.whitelist()
+def calculate_work_experience_and_amount(employee, gratuity_rule):
+ current_work_experience = calculate_work_experience(employee, gratuity_rule) or 0
+ gratuity_amount = calculate_gratuity_amount(employee, gratuity_rule, current_work_experience) or 0
+
+ return {'current_work_experience': current_work_experience, "amount": gratuity_amount}
+
+def calculate_work_experience(employee, gratuity_rule):
+
+ total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value("Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"])
+
+ date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
+ if not relieving_date:
+ frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(get_link_to_form("Employee", employee))))
+
+ method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function")
+ employee_total_workings_days = calculate_employee_total_workings_days(employee, date_of_joining, relieving_date)
+
+ current_work_experience = employee_total_workings_days/total_working_days_per_year or 1
+ current_work_experience = get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee)
+ return current_work_experience
+
+def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date ):
+ employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
+
+ payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") or "Leave"
+ if payroll_based_on == "Leave":
+ total_lwp = get_non_working_days(employee, relieving_date, "On Leave")
+ employee_total_workings_days -= total_lwp
+ elif payroll_based_on == "Attendance":
+ total_absents = get_non_working_days(employee, relieving_date, "Absent")
+ employee_total_workings_days -= total_absents
+
+ return employee_total_workings_days
+
+def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee):
+ if method == "Round off Work Experience":
+ current_work_experience = round(current_work_experience)
+ else:
+ current_work_experience = floor(current_work_experience)
+
+ if current_work_experience < minimum_year_for_gratuity:
+ frappe.throw(_("Employee: {0} have to complete minimum {1} years for gratuity").format(bold(employee), minimum_year_for_gratuity))
+ return current_work_experience
+
+def get_non_working_days(employee, relieving_date, status):
+
+ filters={
+ "docstatus": 1,
+ "status": status,
+ "employee": employee,
+ "attendance_date": ("<=", get_datetime(relieving_date))
+ }
+
+ if status == "On Leave":
+ lwp_leave_types = frappe.get_list("Leave Type", filters = {"is_lwp":1})
+ lwp_leave_types = [leave_type.name for leave_type in lwp_leave_types]
+ filters["leave_type"] = ("IN", lwp_leave_types)
+
+
+ record = frappe.get_all("Attendance", filters=filters, fields = ["COUNT(name) as total_lwp"])
+ return record[0].total_lwp if len(record) else 0
+
+def calculate_gratuity_amount(employee, gratuity_rule, experience):
+ applicable_earnings_component = get_applicable_components(gratuity_rule)
+ total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule)
+
+ calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on")
+ gratuity_amount = 0
+ slabs = get_gratuity_rule_slabs(gratuity_rule)
+ slab_found = False
+ year_left = experience
+
+ for slab in slabs:
+ if calculate_gratuity_amount_based_on == "Current Slab":
+ slab_found, gratuity_amount = calculate_amount_based_on_current_slab(slab.from_year, slab.to_year,
+ experience, total_applicable_components_amount, slab.fraction_of_applicable_earnings)
+ if slab_found:
+ break
+
+ elif calculate_gratuity_amount_based_on == "Sum of all previous slabs":
+ if slab.to_year == 0 and slab.from_year == 0:
+ gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings
+ slab_found = True
+ break
+
+ if experience > slab.to_year and experience > slab.from_year and slab.to_year !=0:
+ gratuity_amount += (slab.to_year - slab.from_year) * total_applicable_components_amount * slab.fraction_of_applicable_earnings
+ year_left -= (slab.to_year - slab.from_year)
+ slab_found = True
+ elif slab.from_year <= experience and (experience < slab.to_year or slab.to_year == 0):
+ gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings
+ slab_found = True
+
+ if not slab_found:
+ frappe.throw(_("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format(bold(gratuity_rule)))
+ return gratuity_amount
+
+def get_applicable_components(gratuity_rule):
+ applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"])
+ if len(applicable_earnings_component) == 0:
+ frappe.throw(_("No Applicable Earnings Component found for Gratuity Rule: {0}").format(bold(get_link_to_form("Gratuity Rule",gratuity_rule))))
+ applicable_earnings_component = [component.salary_component for component in applicable_earnings_component]
+
+ return applicable_earnings_component
+
+def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule):
+ sal_slip = get_last_salary_slip(employee)
+ if not sal_slip:
+ frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee)))
+ component_and_amounts = frappe.get_list("Salary Detail",
+ filters={
+ "docstatus": 1,
+ 'parent': sal_slip,
+ "parentfield": "earnings",
+ 'salary_component': ('in', applicable_earnings_component)
+ },
+ fields=["amount"])
+ total_applicable_components_amount = 0
+ if not len(component_and_amounts):
+ frappe.throw(_("No Applicable Component is present in last month salary slip"))
+ for data in component_and_amounts:
+ total_applicable_components_amount += data.amount
+ return total_applicable_components_amount
+
+def calculate_amount_based_on_current_slab(from_year, to_year, experience, total_applicable_components_amount, fraction_of_applicable_earnings):
+ slab_found = False; gratuity_amount = 0
+ if experience >= from_year and (to_year == 0 or experience < to_year):
+ gratuity_amount = total_applicable_components_amount * experience * fraction_of_applicable_earnings
+ if fraction_of_applicable_earnings:
+ slab_found = True
+
+ return slab_found, gratuity_amount
+
+def get_gratuity_rule_slabs(gratuity_rule):
+ return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx")
+
+def get_salary_structure(employee):
+ return frappe.get_list("Salary Structure Assignment", filters = {
+ "employee": employee, 'docstatus': 1
+ },
+ fields=["from_date", "salary_structure"],
+ order_by = "from_date desc")[0].salary_structure
+
+def get_last_salary_slip(employee):
+ return frappe.get_list("Salary Slip", filters = {
+ "employee": employee, 'docstatus': 1
+ },
+ order_by = "start_date desc")[0].name
+
diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
new file mode 100644
index 00000000000..5b2489f22cd
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
@@ -0,0 +1,20 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'reference_name',
+ 'non_standard_fieldnames': {
+ 'Additional Salary': 'ref_docname',
+ },
+ 'transactions': [
+ {
+ 'label': _('Payment'),
+ 'items': ['Payment Entry']
+ },
+ {
+ 'label': _('Additional Salary'),
+ 'items': ['Additional Salary']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
new file mode 100644
index 00000000000..e89e3dd077a
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -0,0 +1,192 @@
+# -*- 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.employee.test_employee import make_employee
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, make_earning_salary_component, \
+ make_deduction_salary_component
+from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
+from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
+from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
+from frappe.utils import getdate, add_days, get_datetime, flt
+
+test_dependencies = ["Salary Component", "Salary Slip", "Account"]
+class TestGratuity(unittest.TestCase):
+ def setUp(self):
+ make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
+ make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
+ frappe.db.sql("DELETE FROM `tabGratuity`")
+ frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
+
+ def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
+ employee, sal_slip = create_employee_and_get_last_salary_slip()
+
+ rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
+
+ gratuity = create_gratuity(pay_via_salary_slip = 1, employee=employee, rule=rule.name)
+
+ #work experience calculation
+ date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
+ employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
+
+ experience = employee_total_workings_days/rule.total_working_days_per_year
+ gratuity.reload()
+ from math import floor
+ self.assertEqual(floor(experience), gratuity.current_work_experience)
+
+ #amount Calculation
+ component_amount = frappe.get_list("Salary Detail",
+ filters={
+ "docstatus": 1,
+ 'parent': sal_slip,
+ "parentfield": "earnings",
+ 'salary_component': "Basic Salary"
+ },
+ fields=["amount"])
+
+ ''' 5 - 0 fraction is 1 '''
+
+ gratuity_amount = component_amount[0].amount * experience
+ gratuity.reload()
+
+ self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
+
+ #additional salary creation (Pay via salary slip)
+ self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
+
+ def test_check_gratuity_amount_based_on_all_previous_slabs(self):
+ employee, sal_slip = create_employee_and_get_last_salary_slip()
+ rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
+ set_mode_of_payment_account()
+
+ gratuity = create_gratuity(expense_account = 'Payment Account - _TC', mode_of_payment='Cash', employee=employee)
+
+ #work experience calculation
+ date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
+ employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
+
+ experience = employee_total_workings_days/rule.total_working_days_per_year
+
+ gratuity.reload()
+
+ from math import floor
+
+ self.assertEqual(floor(experience), gratuity.current_work_experience)
+
+ #amount Calculation
+ component_amount = frappe.get_list("Salary Detail",
+ filters={
+ "docstatus": 1,
+ 'parent': sal_slip,
+ "parentfield": "earnings",
+ 'salary_component': "Basic Salary"
+ },
+ fields=["amount"])
+
+ ''' range | Fraction
+ 0-1 | 0
+ 1-5 | 0.7
+ 5-0 | 1
+ '''
+
+ gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
+ gratuity.reload()
+
+ self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
+ self.assertEqual(gratuity.status, "Unpaid")
+
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+ pay_entry = get_payment_entry("Gratuity", gratuity.name)
+ pay_entry.reference_no = "123467"
+ pay_entry.reference_date = getdate()
+ pay_entry.save()
+ pay_entry.submit()
+ gratuity.reload()
+
+ self.assertEqual(gratuity.status, "Paid")
+ self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2))
+
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabGratuity`")
+ frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
+
+def get_gratuity_rule(name):
+ rule = frappe.db.exists("Gratuity Rule", name)
+ if not rule:
+ create_gratuity_rule()
+ rule = frappe.get_doc("Gratuity Rule", name)
+ rule.applicable_earnings_component = []
+ rule.append("applicable_earnings_component", {
+ "salary_component": "Basic Salary"
+ })
+ rule.save()
+ rule.reload()
+
+ return rule
+
+def create_gratuity(**args):
+ if args:
+ args = frappe._dict(args)
+ gratuity = frappe.new_doc("Gratuity")
+ gratuity.employee = args.employee
+ gratuity.posting_date = getdate()
+ gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)"
+ gratuity.pay_via_salary_slip = args.pay_via_salary_slip or 0
+ if gratuity.pay_via_salary_slip:
+ gratuity.payroll_date = getdate()
+ gratuity.salary_component = "Performance Bonus"
+ else:
+ gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
+ gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
+ gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
+
+ gratuity.save()
+ gratuity.submit()
+
+ return gratuity
+
+def set_mode_of_payment_account():
+ if not frappe.db.exists("Account", "Payment Account - _TC"):
+ mode_of_payment = create_account()
+
+ mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
+
+ mode_of_payment.accounts = []
+ mode_of_payment.append("accounts", {
+ "company": "_Test Company",
+ "default_account": "_Test Bank - _TC"
+ })
+ mode_of_payment.save()
+
+def create_account():
+ return frappe.get_doc({
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Payment Account",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Bank Accounts - _TC",
+ "account_type": "Bank",
+ }).insert(ignore_permissions=True)
+
+def create_employee_and_get_last_salary_slip():
+ employee = make_employee("test_employee@salary.com", company='_Test Company')
+ frappe.db.set_value("Employee", employee, "relieving_date", getdate())
+ frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), - (6*365)))
+ if not frappe.db.exists("Salary Slip", {"employee":employee}):
+ salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
+ salary_slip.submit()
+ salary_slip = salary_slip.name
+ else:
+ salary_slip = get_last_salary_slip(employee)
+
+ if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+ make_holiday_list()
+ frappe.db.set_value("Company", '_Test Company', "default_holiday_list", "Salary Slip Test Holiday List")
+
+ return employee, salary_slip
diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py
similarity index 100%
rename from erpnext/non_profit/doctype/membership_settings/__init__.py
rename to erpnext/payroll/doctype/gratuity_applicable_component/__init__.py
diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
new file mode 100644
index 00000000000..eea0e852b17
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
@@ -0,0 +1,32 @@
+{
+ "actions": [],
+ "creation": "2020-08-05 19:00:28.097265",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "salary_component"
+ ],
+ "fields": [
+ {
+ "fieldname": "salary_component",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Salary Component ",
+ "options": "Salary Component",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-05 20:17:13.855035",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity Applicable Component",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
similarity index 55%
rename from erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py
rename to erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
index bf0a590d484..23e4340b04f 100644
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py
+++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class BankStatementTransactionSettingsItem(Document):
+class GratuityApplicableComponent(Document):
pass
diff --git a/erpnext/payroll/doctype/gratuity_rule/__init__.py b/erpnext/payroll/doctype/gratuity_rule/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
new file mode 100644
index 00000000000..ee6c5df7371
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Gratuity Rule', {
+ // refresh: function(frm) {
+
+ // }
+});
+
+frappe.ui.form.on('Gratuity Rule Slab', {
+
+ /*
+ Slabs should be in order like
+
+ from | to | fraction
+ 0 | 4 | 0.5
+ 4 | 6 | 0.7
+
+ So, on row addition setting current_row.from = previous row.to.
+ On to_year insert we have to check that it is not less than from_year
+
+ Wrong order may lead to Wrong Calculation
+ */
+
+ gratuity_rule_slabs_add(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ let array_idx = row.idx - 1;
+ if (array_idx > 0) {
+ row.from_year = cur_frm.doc.gratuity_rule_slabs[array_idx - 1].to_year;
+ frm.refresh();
+ }
+ },
+
+ to_year(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ if (row.to_year <= row.from_year && row.to_year === 0) {
+ frappe.throw(__("To(Year) year can not be less than From(year) "));
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json
new file mode 100644
index 00000000000..84cdcf50386
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json
@@ -0,0 +1,114 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2020-08-05 19:00:36.103500",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "applicable_earnings_component",
+ "work_experience_calculation_function",
+ "total_working_days_per_year",
+ "column_break_3",
+ "disable",
+ "calculate_gratuity_amount_based_on",
+ "minimum_year_for_gratuity",
+ "gratuity_rules_section",
+ "gratuity_rule_slabs"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "disable",
+ "fieldtype": "Check",
+ "label": "Disable"
+ },
+ {
+ "fieldname": "calculate_gratuity_amount_based_on",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Calculate Gratuity Amount Based On",
+ "options": "Current Slab\nSum of all previous slabs",
+ "reqd": 1
+ },
+ {
+ "description": "Salary components should be part of the Salary Structure.",
+ "fieldname": "applicable_earnings_component",
+ "fieldtype": "Table MultiSelect",
+ "label": "Applicable Earnings Component",
+ "options": "Gratuity Applicable Component",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "gratuity_rules_section",
+ "fieldtype": "Section Break",
+ "label": "Gratuity Rules"
+ },
+ {
+ "description": "Leave
From and
To 0 for no upper and lower limit.",
+ "fieldname": "gratuity_rule_slabs",
+ "fieldtype": "Table",
+ "label": "Current Work Experience",
+ "options": "Gratuity Rule Slab",
+ "reqd": 1
+ },
+ {
+ "default": "Round off Work Experience",
+ "fieldname": "work_experience_calculation_function",
+ "fieldtype": "Select",
+ "label": "Work Experience Calculation method",
+ "options": "Round off Work Experience\nTake Exact Completed Years"
+ },
+ {
+ "default": "365",
+ "fieldname": "total_working_days_per_year",
+ "fieldtype": "Int",
+ "label": "Total working Days Per Year"
+ },
+ {
+ "fieldname": "minimum_year_for_gratuity",
+ "fieldtype": "Int",
+ "label": "Minimum Year for Gratuity"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-03 17:08:27.891535",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity Rule",
+ "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
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py
new file mode 100644
index 00000000000..29a6ebe1a6a
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py
@@ -0,0 +1,33 @@
+# -*- 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 _
+
+class GratuityRule(Document):
+
+ def validate(self):
+ for current_slab in self.gratuity_rule_slabs:
+ if (current_slab.from_year > current_slab.to_year) and current_slab.to_year != 0:
+ frappe(_("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx))
+
+ if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1:
+ frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits."))
+
+def get_gratuity_rule(name, slabs, **args):
+ args = frappe._dict(args)
+
+ rule = frappe.new_doc("Gratuity Rule")
+ rule.name = name
+ rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab"
+ rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years"
+ rule.minimum_year_for_gratuity = 1
+
+
+ for slab in slabs:
+ slab = frappe._dict(slab)
+ rule.append("gratuity_rule_slabs", slab)
+ return rule
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py
new file mode 100644
index 00000000000..0d70163495a
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'gratuity_rule',
+ 'transactions': [
+ {
+ 'label': _('Gratuity'),
+ 'items': ['Gratuity']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py
new file mode 100644
index 00000000000..1f5dc4e571e
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.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 TestGratuityRule(unittest.TestCase):
+ pass
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py b/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
new file mode 100644
index 00000000000..bc37b0f51ed
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
@@ -0,0 +1,50 @@
+{
+ "actions": [],
+ "creation": "2020-08-05 19:12:49.423500",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "from_year",
+ "to_year",
+ "fraction_of_applicable_earnings"
+ ],
+ "fields": [
+ {
+ "fieldname": "fraction_of_applicable_earnings",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Fraction of Applicable Earnings ",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "from_year",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "From(Year)",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "to_year",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "To(Year)",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-17 14:09:56.781712",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity Rule Slab",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
similarity index 56%
rename from erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py
rename to erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
index cb1b15815fb..fa468e77beb 100644
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py
+++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class BankStatementTransactionInvoiceItem(Document):
+class GratuityRuleSlab(Document):
pass
diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
index c47caa1227b..54377e94b30 100644
--- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
+++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
@@ -15,6 +15,7 @@
"daily_wages_fraction_for_half_day",
"email_salary_slip_to_employee",
"encrypt_salary_slips_in_emails",
+ "show_leave_balances_in_salary_slip",
"password_policy"
],
"fields": [
@@ -23,58 +24,44 @@
"fieldname": "payroll_based_on",
"fieldtype": "Select",
"label": "Calculate Payroll Working Days Based On",
- "options": "Leave\nAttendance",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Leave\nAttendance"
},
{
"fieldname": "max_working_hours_against_timesheet",
"fieldtype": "Float",
- "label": "Max working hours against Timesheet",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Max working hours against Timesheet"
},
{
"default": "0",
"description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day",
"fieldname": "include_holidays_in_total_working_days",
"fieldtype": "Check",
- "label": "Include holidays in Total no. of Working Days",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Include holidays in Total no. of Working Days"
},
{
"default": "0",
"description": "If checked, hides and disables Rounded Total field in Salary Slips",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
- "label": "Disable Rounded Total",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Disable Rounded Total"
},
{
"fieldname": "column_break_11",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"default": "0.5",
"description": "The fraction of daily wages to be paid for half-day attendance",
"fieldname": "daily_wages_fraction_for_half_day",
"fieldtype": "Float",
- "label": "Fraction of Daily Salary for Half Day",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Fraction of Daily Salary for Half Day"
},
{
"default": "1",
"description": "Emails salary slip to employee based on preferred email selected in Employee",
"fieldname": "email_salary_slip_to_employee",
"fieldtype": "Check",
- "label": "Email Salary Slip to Employee",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Email Salary Slip to Employee"
},
{
"default": "0",
@@ -82,9 +69,7 @@
"description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",
"fieldname": "encrypt_salary_slips_in_emails",
"fieldtype": "Check",
- "label": "Encrypt Salary Slips in Emails",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Encrypt Salary Slips in Emails"
},
{
"depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1",
@@ -92,24 +77,27 @@
"fieldname": "password_policy",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Password Policy",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Password Policy"
},
{
"depends_on": "eval:doc.payroll_based_on == 'Attendance'",
"fieldname": "consider_unmarked_attendance_as",
"fieldtype": "Select",
"label": "Consider Unmarked Attendance As",
- "options": "Present\nAbsent",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Present\nAbsent"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_leave_balances_in_salary_slip",
+ "fieldtype": "Check",
+ "label": "Show Leave Balances in Salary Slip"
}
],
"icon": "fa fa-cog",
+ "index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-06-22 17:00:58.408030",
+ "modified": "2021-03-03 17:49:59.579723",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Settings",
@@ -126,5 +114,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 9f9691b59d1..66883682625 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -80,6 +80,8 @@
"total_in_words",
"column_break_69",
"base_total_in_words",
+ "leave_details_section",
+ "leave_details",
"section_break_75",
"amended_from"
],
@@ -612,13 +614,25 @@
"label": "Month To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fieldname": "leave_details_section",
+ "fieldtype": "Section Break",
+ "label": "Leave Details"
+ },
+ {
+ "fieldname": "leave_details",
+ "fieldtype": "Table",
+ "label": "Leave Details",
+ "options": "Salary Slip Leave",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-14 13:37:38.180920",
+ "modified": "2021-02-19 11:48:05.383945",
"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 2d3bc57900d..595d6974fd5 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -19,6 +19,7 @@ from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_appli
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year
+from six import iteritems
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@@ -53,6 +54,7 @@ class SalarySlip(TransactionBase):
self.compute_year_to_date()
self.compute_month_to_date()
self.compute_component_wise_year_to_date()
+ self.add_leave_balances()
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")
@@ -78,9 +80,26 @@ class SalarySlip(TransactionBase):
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip()
+ self.update_payment_status_for_gratuity()
+
+ def update_payment_status_for_gratuity(self):
+ add_salary = frappe.db.get_all("Additional Salary",
+ filters = {
+ "payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
+ "employee": self.employee,
+ "ref_doctype": "Gratuity",
+ "docstatus": 1,
+ }, fields = ["ref_docname", "name"], limit=1)
+
+ if len(add_salary):
+ status = "Paid" if self.docstatus == 1 else "Unpaid"
+ if add_salary[0].name in [data.additional_salary for data in self.earnings]:
+ frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
+
def on_cancel(self):
self.set_status()
self.update_status()
+ self.update_payment_status_for_gratuity()
self.cancel_loan_repayment_entry()
def on_trash(self):
@@ -504,7 +523,8 @@ class SalarySlip(TransactionBase):
return amount
except NameError as err:
- frappe.throw(_("Name error: {0}").format(err))
+ frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
except SyntaxError as err:
frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
except Exception as e:
@@ -571,6 +591,7 @@ class SalarySlip(TransactionBase):
for d in self.get(key):
if d.salary_component == struct_row.salary_component:
component_row = d
+
if not component_row or (struct_row.get("is_additional_component") and not overwrite):
if amount:
self.append(key, {
@@ -928,7 +949,8 @@ class SalarySlip(TransactionBase):
if condition:
return frappe.safe_eval(condition, self.whitelisted_globals, data)
except NameError as err:
- frappe.throw(_("Name error: {0}").format(err))
+ frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
except SyntaxError as err:
frappe.throw(_("Syntax error in condition: {0}").format(err))
except Exception as e:
@@ -1103,10 +1125,10 @@ class SalarySlip(TransactionBase):
self.calculate_total_for_salary_slip_based_on_timesheet()
else:
self.total_deduction = 0.0
- if self.earnings:
+ if hasattr(self, "earnings"):
for earning in self.earnings:
self.gross_pay += flt(earning.amount, earning.precision("amount"))
- if self.deductions:
+ if hasattr(self, "deductions"):
for deduction in self.deductions:
self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
@@ -1123,6 +1145,7 @@ class SalarySlip(TransactionBase):
#calculate total working hours, earnings based on hourly wages and totals
def calculate_total_for_salary_slip_based_on_timesheet(self):
if self.timesheets:
+ self.total_working_hours = 0
for timesheet in self.timesheets:
if timesheet.working_hours:
self.total_working_hours += timesheet.working_hours
@@ -1212,6 +1235,22 @@ class SalarySlip(TransactionBase):
return period_start_date, period_end_date
+ def add_leave_balances(self):
+ self.set('leave_details', [])
+
+ if frappe.db.get_single_value('Payroll Settings', 'show_leave_balances_in_salary_slip'):
+ from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
+ leave_details = get_leave_details(self.employee, self.end_date)
+
+ for leave_type, leave_values in iteritems(leave_details['leave_allocation']):
+ self.append('leave_details', {
+ 'leave_type': leave_type,
+ 'total_allocated_leaves': flt(leave_values.get('total_leaves')),
+ 'expired_leaves': flt(leave_values.get('expired_leaves')),
+ 'used_leaves': flt(leave_values.get('leaves_taken')),
+ 'pending_leaves': flt(leave_values.get('pending_leaves')),
+ 'available_leaves': flt(leave_values.get('remaining_leaves'))
+ })
def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
@@ -1223,4 +1262,4 @@ def unlink_ref_doc_from_salary_slip(ref_no):
def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee)
- return policy_template.format(**employee.as_dict())
\ No newline at end of file
+ return policy_template.format(**employee.as_dict())
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index f58a8e58c20..143a306eb34 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -21,6 +21,7 @@ from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_ta
class TestSalarySlip(unittest.TestCase):
def setUp(self):
setup_test()
+
def tearDown(self):
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator")
@@ -245,7 +246,7 @@ class TestSalarySlip(unittest.TestCase):
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period)
- frappe.db.sql("""delete from `tabLoan""")
+ 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()
diff --git a/erpnext/payroll/doctype/salary_slip_leave/__init__.py b/erpnext/payroll/doctype/salary_slip_leave/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
new file mode 100644
index 00000000000..7ac453b3c3d
--- /dev/null
+++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
@@ -0,0 +1,78 @@
+{
+ "actions": [],
+ "creation": "2021-02-19 11:45:18.173417",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "leave_type",
+ "total_allocated_leaves",
+ "expired_leaves",
+ "used_leaves",
+ "pending_leaves",
+ "available_leaves"
+ ],
+ "fields": [
+ {
+ "fieldname": "leave_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Leave Type",
+ "no_copy": 1,
+ "options": "Leave Type",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_allocated_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Total Allocated Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "expired_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Expired Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "used_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Used Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "pending_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Pending Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "available_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Available Leave",
+ "no_copy": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-19 10:47:48.546724",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Salary Slip Leave",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.py
similarity index 58%
rename from erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py
rename to erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.py
index 9438e9a63f0..7a92bf18f76 100644
--- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py
+++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2018, sathishpy@gmail.com and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
-class BankStatementSettingsItem(Document):
+class SalarySlipLeave(Document):
pass
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 1378bf0b913..6aa13873633 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -142,6 +142,8 @@ frappe.ui.form.on('Salary Structure', {
],
primary_action: function() {
var data = d.get_values();
+ delete data.company
+ delete data.currency
frappe.call({
doc: frm.doc,
method: "assign_salary_structure",
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 3570a0f2be4..077011ace07 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -75,24 +75,27 @@ frappe.ui.form.on("Project", {
frm.add_custom_button(__('Cancelled'), () => {
frm.events.set_status(frm, 'Cancelled');
}, __('Set Status'));
- }
- if (frappe.model.can_read("Task")) {
- frm.add_custom_button(__("Gantt Chart"), function () {
- frappe.route_options = {
- "project": frm.doc.name
- };
- frappe.set_route("List", "Task", "Gantt");
- });
- frm.add_custom_button(__("Kanban Board"), () => {
- frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
- project: frm.doc.project_name
- }).then(() => {
- frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
+ if (frappe.model.can_read("Task")) {
+ frm.add_custom_button(__("Gantt Chart"), function () {
+ frappe.route_options = {
+ "project": frm.doc.name
+ };
+ frappe.set_route("List", "Task", "Gantt");
});
- });
+
+ frm.add_custom_button(__("Kanban Board"), () => {
+ frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
+ project: frm.doc.project_name
+ }).then(() => {
+ frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
+ });
+ });
+ }
}
+
+
},
create_duplicate: function(frm) {
@@ -135,4 +138,4 @@ function open_form(frm, doctype, child_doctype, parentfield) {
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
-}
\ No newline at end of file
+}
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index d85c82612a2..62905385a31 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -37,7 +37,7 @@ class TestProject(unittest.TestCase):
task1 = task_exists("Test Template Task Parent")
if not task1:
- task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
+ task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=4)
task2 = task_exists("Test Template Task Child 1")
if not task2:
@@ -52,7 +52,7 @@ class TestProject(unittest.TestCase):
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 4))
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json
index 69530b15b40..16caaa20ae4 100644
--- a/erpnext/projects/doctype/project_template_task/project_template_task.json
+++ b/erpnext/projects/doctype/project_template_task/project_template_task.json
@@ -20,6 +20,7 @@
},
{
"columns": 6,
+ "fetch_from": "task.subject",
"fieldname": "subject",
"fieldtype": "Read Only",
"in_list_view": 1,
@@ -28,7 +29,7 @@
],
"istable": 1,
"links": [],
- "modified": "2021-01-07 15:13:40.995071",
+ "modified": "2021-02-24 15:18:49.095071",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index a2095c95d51..855ff5f83e8 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -17,312 +17,326 @@ class CircularReferenceError(frappe.ValidationError): pass
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet):
- nsm_parent_field = 'parent_task'
+ nsm_parent_field = 'parent_task'
- def get_feed(self):
- return '{0}: {1}'.format(_(self.status), self.subject)
+ def get_feed(self):
+ return '{0}: {1}'.format(_(self.status), self.subject)
- def get_customer_details(self):
- cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
- if cust:
- ret = {'customer_name': cust and cust[0][0] or ''}
- return ret
+ def get_customer_details(self):
+ cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
+ if cust:
+ ret = {'customer_name': cust and cust[0][0] or ''}
+ return ret
- def validate(self):
- self.validate_dates()
- self.validate_parent_project_dates()
- self.validate_progress()
- self.validate_status()
- self.update_depends_on()
- self.validate_dependencies_for_template_task()
+ def validate(self):
+ self.validate_dates()
+ self.validate_parent_expected_end_date()
+ self.validate_parent_project_dates()
+ self.validate_progress()
+ self.validate_status()
+ self.update_depends_on()
+ self.validate_dependencies_for_template_task()
- def validate_dates(self):
- if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
- frappe.bold("Expected End Date")))
+ def validate_dates(self):
+ if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+ frappe.bold("Expected End Date")))
- if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
- frappe.bold("Actual End Date")))
+ if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+ frappe.bold("Actual End Date")))
- def validate_parent_project_dates(self):
- if not self.project or frappe.flags.in_test:
- return
+ def validate_parent_expected_end_date(self):
+ if self.parent_task:
+ parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
+ if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
+ frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
- expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
+ def validate_parent_project_dates(self):
+ if not self.project or frappe.flags.in_test:
+ return
- if expected_end_date:
- validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
- validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
+ expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
- def validate_status(self):
- if self.is_template and self.status != "Template":
- self.status = "Template"
- if self.status!=self.get_db_value("status") and self.status == "Completed":
- for d in self.depends_on:
- if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
- frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
+ if expected_end_date:
+ validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
+ validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
- close_all_assignments(self.doctype, self.name)
+ def validate_status(self):
+ if self.is_template and self.status != "Template":
+ self.status = "Template"
+ if self.status!=self.get_db_value("status") and self.status == "Completed":
+ for d in self.depends_on:
+ if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
+ frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
- def validate_progress(self):
- if flt(self.progress or 0) > 100:
- frappe.throw(_("Progress % for a task cannot be more than 100."))
+ close_all_assignments(self.doctype, self.name)
- if flt(self.progress) == 100:
- self.status = 'Completed'
+ def validate_progress(self):
+ if flt(self.progress or 0) > 100:
+ frappe.throw(_("Progress % for a task cannot be more than 100."))
- if self.status == 'Completed':
- self.progress = 100
+ if flt(self.progress) == 100:
+ self.status = 'Completed'
- def validate_dependencies_for_template_task(self):
- if self.is_template:
- self.validate_parent_template_task()
- self.validate_depends_on_tasks()
-
- def validate_parent_template_task(self):
- if self.parent_task:
- if not frappe.db.get_value("Task", self.parent_task, "is_template"):
- parent_task_format = """
{0} """.format(self.parent_task)
- frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
-
- def validate_depends_on_tasks(self):
- if self.depends_on:
- for task in self.depends_on:
- if not frappe.db.get_value("Task", task.task, "is_template"):
- dependent_task_format = """
{0} """.format(task.task)
- frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
+ if self.status == 'Completed':
+ self.progress = 100
- def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
- for d in self.depends_on:
- if d.task and d.task not in depends_on_tasks:
- depends_on_tasks += d.task + ","
- self.depends_on_tasks = depends_on_tasks
+ def validate_dependencies_for_template_task(self):
+ if self.is_template:
+ self.validate_parent_template_task()
+ self.validate_depends_on_tasks()
- def update_nsm_model(self):
- frappe.utils.nestedset.update_nsm(self)
+ def validate_parent_template_task(self):
+ if self.parent_task:
+ if not frappe.db.get_value("Task", self.parent_task, "is_template"):
+ parent_task_format = """
{0} """.format(self.parent_task)
+ frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
- def on_update(self):
- self.update_nsm_model()
- self.check_recursion()
- self.reschedule_dependent_tasks()
- self.update_project()
- self.unassign_todo()
- self.populate_depends_on()
+ def validate_depends_on_tasks(self):
+ if self.depends_on:
+ for task in self.depends_on:
+ if not frappe.db.get_value("Task", task.task, "is_template"):
+ dependent_task_format = """
{0} """.format(task.task)
+ frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
- def unassign_todo(self):
- if self.status == "Completed":
- close_all_assignments(self.doctype, self.name)
- if self.status == "Cancelled":
- clear(self.doctype, self.name)
+ def update_depends_on(self):
+ depends_on_tasks = self.depends_on_tasks or ""
+ for d in self.depends_on:
+ if d.task and d.task not in depends_on_tasks:
+ depends_on_tasks += d.task + ","
+ self.depends_on_tasks = depends_on_tasks
- def update_total_expense_claim(self):
- self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
- where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
+ def update_nsm_model(self):
+ frappe.utils.nestedset.update_nsm(self)
- def update_time_and_costing(self):
- tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
- sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
- sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
- ,self.name, as_dict=1)[0]
- if self.status == "Open":
- self.status = "Working"
- self.total_costing_amount= tl.total_costing_amount
- self.total_billing_amount= tl.total_billing_amount
- self.actual_time= tl.time
- self.act_start_date= tl.start_date
- self.act_end_date= tl.end_date
+ def on_update(self):
+ self.update_nsm_model()
+ self.check_recursion()
+ self.reschedule_dependent_tasks()
+ self.update_project()
+ self.unassign_todo()
+ self.populate_depends_on()
- def update_project(self):
- if self.project and not self.flags.from_project:
- frappe.get_cached_doc("Project", self.project).update_project()
+ def unassign_todo(self):
+ if self.status == "Completed":
+ close_all_assignments(self.doctype, self.name)
+ if self.status == "Cancelled":
+ clear(self.doctype, self.name)
- def check_recursion(self):
- if self.flags.ignore_recursion_check: return
- check_list = [['task', 'parent'], ['parent', 'task']]
- for d in check_list:
- task_list, count = [self.name], 0
- while (len(task_list) > count ):
- tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
- (d[0], d[1], '%s'), cstr(task_list[count]))
- count = count + 1
- for b in tasks:
- if b[0] == self.name:
- frappe.throw(_("Circular Reference Error"), CircularReferenceError)
- if b[0]:
- task_list.append(b[0])
+ def update_total_expense_claim(self):
+ self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
+ where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
- if count == 15:
- break
+ def update_time_and_costing(self):
+ tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
+ sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
+ sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
+ ,self.name, as_dict=1)[0]
+ if self.status == "Open":
+ self.status = "Working"
+ self.total_costing_amount= tl.total_costing_amount
+ self.total_billing_amount= tl.total_billing_amount
+ self.actual_time= tl.time
+ self.act_start_date= tl.start_date
+ self.act_end_date= tl.end_date
- def reschedule_dependent_tasks(self):
- end_date = self.exp_end_date or self.act_end_date
- if end_date:
- for task_name in frappe.db.sql("""
- select name from `tabTask` as parent
- where parent.project = %(project)s
- and parent.name in (
- select parent from `tabTask Depends On` as child
- where child.task = %(task)s and child.project = %(project)s)
- """, {'project': self.project, 'task':self.name }, as_dict=1):
- task = frappe.get_doc("Task", task_name.name)
- if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
- task_duration = date_diff(task.exp_end_date, task.exp_start_date)
- task.exp_start_date = add_days(end_date, 1)
- task.exp_end_date = add_days(task.exp_start_date, task_duration)
- task.flags.ignore_recursion_check = True
- task.save()
+ def update_project(self):
+ if self.project and not self.flags.from_project:
+ frappe.get_cached_doc("Project", self.project).update_project()
- def has_webform_permission(self):
- project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
- if project_user:
- return True
+ def check_recursion(self):
+ if self.flags.ignore_recursion_check: return
+ check_list = [['task', 'parent'], ['parent', 'task']]
+ for d in check_list:
+ task_list, count = [self.name], 0
+ while (len(task_list) > count ):
+ tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
+ (d[0], d[1], '%s'), cstr(task_list[count]))
+ count = count + 1
+ for b in tasks:
+ if b[0] == self.name:
+ frappe.throw(_("Circular Reference Error"), CircularReferenceError)
+ if b[0]:
+ task_list.append(b[0])
- def populate_depends_on(self):
- if self.parent_task:
- parent = frappe.get_doc('Task', self.parent_task)
- if self.name not in [row.task for row in parent.depends_on]:
- parent.append("depends_on", {
- "doctype": "Task Depends On",
- "task": self.name,
- "subject": self.subject
- })
- parent.save()
+ if count == 15:
+ break
- def on_trash(self):
- if check_if_child_exists(self.name):
- throw(_("Child Task exists for this Task. You can not delete this Task."))
+ def reschedule_dependent_tasks(self):
+ end_date = self.exp_end_date or self.act_end_date
+ if end_date:
+ for task_name in frappe.db.sql("""
+ select name from `tabTask` as parent
+ where parent.project = %(project)s
+ and parent.name in (
+ select parent from `tabTask Depends On` as child
+ where child.task = %(task)s and child.project = %(project)s)
+ """, {'project': self.project, 'task':self.name }, as_dict=1):
+ task = frappe.get_doc("Task", task_name.name)
+ if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
+ task_duration = date_diff(task.exp_end_date, task.exp_start_date)
+ task.exp_start_date = add_days(end_date, 1)
+ task.exp_end_date = add_days(task.exp_start_date, task_duration)
+ task.flags.ignore_recursion_check = True
+ task.save()
- self.update_nsm_model()
+ def has_webform_permission(self):
+ project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
+ if project_user:
+ return True
- def after_delete(self):
- self.update_project()
+ def populate_depends_on(self):
+ if self.parent_task:
+ parent = frappe.get_doc('Task', self.parent_task)
+ if self.name not in [row.task for row in parent.depends_on]:
+ parent.append("depends_on", {
+ "doctype": "Task Depends On",
+ "task": self.name,
+ "subject": self.subject
+ })
+ parent.save()
- def update_status(self):
- if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
- from datetime import datetime
- if self.exp_end_date < datetime.now().date():
- self.db_set('status', 'Overdue', update_modified=False)
- self.update_project()
+ def on_trash(self):
+ if check_if_child_exists(self.name):
+ throw(_("Child Task exists for this Task. You can not delete this Task."))
+
+ self.update_nsm_model()
+
+ def after_delete(self):
+ self.update_project()
+
+ def update_status(self):
+ if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
+ from datetime import datetime
+ if self.exp_end_date < datetime.now().date():
+ self.db_set('status', 'Overdue', update_modified=False)
+ self.update_project()
@frappe.whitelist()
def check_if_child_exists(name):
- child_tasks = frappe.get_all("Task", filters={"parent_task": name})
- child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
- return child_tasks
+ child_tasks = frappe.get_all("Task", filters={"parent_task": name})
+ child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
+ return child_tasks
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
- return frappe.db.sql(""" select name from `tabProject`
- where %(key)s like %(txt)s
- %(mcond)s
- order by name
- limit %(start)s, %(page_len)s""" % {
- 'key': searchfield,
- 'txt': frappe.db.escape('%' + txt + '%'),
- 'mcond':get_match_cond(doctype),
- 'start': start,
- 'page_len': page_len
- })
+ from erpnext.controllers.queries import get_match_cond
+ meta = frappe.get_meta(doctype)
+ searchfields = meta.get_search_fields()
+ search_columns = ", " + ", ".join(searchfields) if searchfields else ''
+ search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
+
+ return frappe.db.sql(""" select name {search_columns} from `tabProject`
+ where %(key)s like %(txt)s
+ %(mcond)s
+ {search_condition}
+ order by name
+ limit %(start)s, %(page_len)s""".format(search_columns = search_columns,
+ search_condition=search_cond), {
+ 'key': searchfield,
+ 'txt': '%' + txt + '%',
+ 'mcond':get_match_cond(doctype),
+ 'start': start,
+ 'page_len': page_len
+ })
@frappe.whitelist()
def set_multiple_status(names, status):
- names = json.loads(names)
- for name in names:
- task = frappe.get_doc("Task", name)
- task.status = status
- task.save()
+ names = json.loads(names)
+ for name in names:
+ task = frappe.get_doc("Task", name)
+ task.status = status
+ task.save()
def set_tasks_as_overdue():
- tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
- for task in tasks:
- if task.status == "Pending Review":
- if getdate(task.review_date) > getdate(today()):
- continue
- frappe.get_doc("Task", task.name).update_status()
+ tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
+ for task in tasks:
+ if task.status == "Pending Review":
+ if getdate(task.review_date) > getdate(today()):
+ continue
+ frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
- def set_missing_values(source, target):
- target.append("time_logs", {
- "hours": source.actual_time,
- "completed": source.status == "Completed",
- "project": source.project,
- "task": source.name
- })
+ def set_missing_values(source, target):
+ target.append("time_logs", {
+ "hours": source.actual_time,
+ "completed": source.status == "Completed",
+ "project": source.project,
+ "task": source.name
+ })
- doclist = get_mapped_doc("Task", source_name, {
- "Task": {
- "doctype": "Timesheet"
- }
- }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
+ doclist = get_mapped_doc("Task", source_name, {
+ "Task": {
+ "doctype": "Timesheet"
+ }
+ }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
- return doclist
+ return doclist
@frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False):
- filters = [['docstatus', '<', '2']]
+ filters = [['docstatus', '<', '2']]
- if task:
- filters.append(['parent_task', '=', task])
- elif parent and not is_root:
- # via expand child
- filters.append(['parent_task', '=', parent])
- else:
- filters.append(['ifnull(`parent_task`, "")', '=', ''])
+ if task:
+ filters.append(['parent_task', '=', task])
+ elif parent and not is_root:
+ # via expand child
+ filters.append(['parent_task', '=', parent])
+ else:
+ filters.append(['ifnull(`parent_task`, "")', '=', ''])
- if project:
- filters.append(['project', '=', project])
+ if project:
+ filters.append(['project', '=', project])
- tasks = frappe.get_list(doctype, fields=[
- 'name as value',
- 'subject as title',
- 'is_group as expandable'
- ], filters=filters, order_by='name')
+ tasks = frappe.get_list(doctype, fields=[
+ 'name as value',
+ 'subject as title',
+ 'is_group as expandable'
+ ], filters=filters, order_by='name')
- # return tasks
- return tasks
+ # return tasks
+ return tasks
@frappe.whitelist()
def add_node():
- from frappe.desk.treeview import make_tree_args
- args = frappe.form_dict
- args.update({
- "name_field": "subject"
- })
- args = make_tree_args(**args)
+ from frappe.desk.treeview import make_tree_args
+ args = frappe.form_dict
+ args.update({
+ "name_field": "subject"
+ })
+ args = make_tree_args(**args)
- if args.parent_task == 'All Tasks' or args.parent_task == args.project:
- args.parent_task = None
+ if args.parent_task == 'All Tasks' or args.parent_task == args.project:
+ args.parent_task = None
- frappe.get_doc(args).insert()
+ frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
- data = json.loads(data)
- new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
- new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
+ data = json.loads(data)
+ new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
+ new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
- for d in data:
- if not d.get("subject"): continue
- new_doc['subject'] = d.get("subject")
- new_task = frappe.get_doc(new_doc)
- new_task.insert()
+ for d in data:
+ if not d.get("subject"): continue
+ new_doc['subject'] = d.get("subject")
+ new_task = frappe.get_doc(new_doc)
+ new_task.insert()
def on_doctype_update():
- frappe.db.add_index("Task", ["lft", "rgt"])
+ frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
- if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
- frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
- if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
- frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+ frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index ea81b3eb644..ed02f79c2dd 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -204,14 +204,16 @@ class Timesheet(Document):
ts_detail.billing_rate = 0.0
@frappe.whitelist()
-def get_projectwise_timesheet_data(project, parent=None):
- cond = ''
+def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None):
+ condition = ''
if parent:
- cond = "and parent = %(parent)s"
+ condition = "AND parent = %(parent)s"
+ if from_time and to_time:
+ condition += "AND from_time BETWEEN %(from_time)s AND %(to_time)s"
return frappe.db.sql("""select name, parent, billing_hours, billing_amount as billing_amt
from `tabTimesheet Detail` where parenttype = 'Timesheet' and docstatus=1 and project = %(project)s {0} and billable = 1
- and sales_invoice is null""".format(cond), {'project': project, 'parent': parent}, as_dict=1)
+ and sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 73262382739..7a3cb838a99 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -61,5 +61,10 @@
"selling/page/point_of_sale/pos_past_order_list.js",
"selling/page/point_of_sale/pos_past_order_summary.js",
"selling/page/point_of_sale/pos_controller.js"
+ ],
+ "js/bank-reconciliation-tool.min.js": [
+ "public/js/bank_reconciliation_tool/data_table_manager.js",
+ "public/js/bank_reconciliation_tool/number_card.js",
+ "public/js/bank_reconciliation_tool/dialog_manager.js"
]
}
diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
new file mode 100644
index 00000000000..5bb58faf2fc
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
@@ -0,0 +1,220 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager(
+ this.company,
+ this.bank_account
+ );
+ this.make_dt();
+ }
+
+ make_dt() {
+ var me = this;
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions",
+ args: {
+ bank_account: this.bank_account,
+ },
+ callback: function (response) {
+ me.format_data(response.message);
+ me.get_dt_columns();
+ me.get_datatable();
+ me.set_listeners();
+ },
+ });
+ }
+
+ get_dt_columns() {
+ this.columns = [
+ {
+ name: "Date",
+ editable: false,
+ width: 100,
+ },
+
+ {
+ name: "Party Type",
+ editable: false,
+ width: 95,
+ },
+ {
+ name: "Party",
+ editable: false,
+ width: 100,
+ },
+ {
+ name: "Description",
+ editable: false,
+ width: 350,
+ },
+ {
+ name: "Deposit",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "
" +
+ format_currency(value, this.currency) +
+ " ",
+ },
+ {
+ name: "Withdrawal",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "
" +
+ format_currency(value, this.currency) +
+ " ",
+ },
+ {
+ name: "Unallocated Amount",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "
" +
+ format_currency(value, this.currency) +
+ " ",
+ },
+ {
+ name: "Reference Number",
+ editable: false,
+ width: 140,
+ },
+ {
+ name: "Actions",
+ editable: false,
+ sortable: false,
+ focusable: false,
+ dropdown: false,
+ width: 80,
+ },
+ ];
+ }
+
+ format_data(transactions) {
+ this.transactions = [];
+ if (transactions[0]) {
+ this.currency = transactions[0]["currency"];
+ }
+ this.transaction_dt_map = {};
+ let length;
+ transactions.forEach((row) => {
+ length = this.transactions.push(this.format_row(row));
+ this.transaction_dt_map[row["name"]] = length - 1;
+ });
+ }
+
+ format_row(row) {
+ return [
+ row["date"],
+ row["party_type"],
+ row["party"],
+ row["description"],
+ row["deposit"],
+ row["withdrawal"],
+ row["unallocated_amount"],
+ row["reference_number"],
+ `
+
+ Actions
+
+ `,
+ ];
+ }
+
+ get_datatable() {
+ const datatable_options = {
+ columns: this.columns,
+ data: this.transactions,
+ dynamicRowHeight: true,
+ checkboxColumn: false,
+ inlineFilters: true,
+ };
+ this.datatable = new frappe.DataTable(
+ this.$reconciliation_tool_dt.get(0),
+ datatable_options
+ );
+ $(`.${this.datatable.style.scopeClass} .dt-scrollable`).css(
+ "max-height",
+ "calc(100vh - 400px)"
+ );
+
+ if (this.transactions.length > 0) {
+ this.$reconciliation_tool_dt.show();
+ this.$no_bank_transactions.hide();
+ } else {
+ this.$reconciliation_tool_dt.hide();
+ this.$no_bank_transactions.show();
+ }
+ }
+
+ set_listeners() {
+ var me = this;
+ $(`.${this.datatable.style.scopeClass} .dt-scrollable`).on(
+ "click",
+ `.btn`,
+ function () {
+ me.dialog_manager.show_dialog(
+ $(this).attr("data-name"),
+ (bank_transaction) => me.update_dt_cards(bank_transaction)
+ );
+ return true;
+ }
+ );
+ }
+
+ update_dt_cards(bank_transaction) {
+ const transaction_index = this.transaction_dt_map[
+ bank_transaction.name
+ ];
+ if (bank_transaction.unallocated_amount > 0) {
+ this.transactions[transaction_index] = this.format_row(
+ bank_transaction
+ );
+ } else {
+ this.transactions.splice(transaction_index, 1);
+ }
+ this.datatable.refresh(this.transactions, this.columns);
+
+ if (this.transactions.length == 0) {
+ this.$reconciliation_tool_dt.hide();
+ this.$no_bank_transactions.show();
+ }
+
+ // this.make_dt();
+ this.get_cleared_balance().then(() => {
+ this.cards_manager.$cards[1].set_value(
+ format_currency(this.cleared_balance),
+ this.currency
+ );
+ this.cards_manager.$cards[2].set_value(
+ format_currency(
+ this.bank_statement_closing_balance - this.cleared_balance
+ ),
+ this.currency
+ );
+ this.cards_manager.$cards[2].set_value_color(
+ this.bank_statement_closing_balance - this.cleared_balance == 0
+ ? "text-success"
+ : "text-danger"
+ );
+ });
+ }
+
+ get_cleared_balance() {
+ if (this.bank_account && this.bank_statement_to_date) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: this.bank_account,
+ till_date: this.bank_statement_to_date,
+ },
+ callback: (response) =>
+ (this.cleared_balance = response.message),
+ });
+ }
+ }
+};
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
new file mode 100644
index 00000000000..142fe79ccdc
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -0,0 +1,594 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
+ constructor(company, bank_account) {
+ this.bank_account = bank_account;
+ this.company = company;
+ this.make_dialog();
+ }
+
+ show_dialog(bank_transaction_name, update_dt_cards) {
+ this.bank_transaction_name = bank_transaction_name;
+ this.update_dt_cards = update_dt_cards;
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "Bank Transaction",
+ filters: { name: this.bank_transaction_name },
+ fieldname: [
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "reference_number",
+ "party_type",
+ "party",
+ "unallocated_amount",
+ "allocated_amount",
+ ],
+ },
+ callback: (r) => {
+ if (r.message) {
+ this.bank_transaction = r.message;
+ r.message.payment_entry = 1;
+ this.dialog.set_values(r.message);
+ this.dialog.show();
+ }
+ },
+ });
+ }
+
+ get_linked_vouchers(document_types) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments",
+ args: {
+ bank_transaction_name: this.bank_transaction_name,
+ document_types: document_types,
+ },
+
+ callback: (result) => {
+ const data = result.message;
+
+
+ if (data && data.length > 0) {
+ const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
+ proposals_wrapper.show();
+ this.dialog.fields_dict.no_matching_vouchers.$wrapper.hide();
+ this.data = [];
+ data.forEach((row) => {
+ const reference_date = row[5] ? row[5] : row[8];
+ this.data.push([
+ row[1],
+ row[2],
+ reference_date,
+ format_currency(row[3], row[9]),
+ row[6],
+ row[4],
+ ]);
+ });
+ this.get_dt_columns();
+ this.get_datatable(proposals_wrapper);
+ } else {
+ const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
+ proposals_wrapper.hide();
+ this.dialog.fields_dict.no_matching_vouchers.$wrapper.show();
+
+ }
+ this.dialog.show();
+ },
+ });
+ }
+
+ get_dt_columns() {
+ this.columns = [
+ {
+ name: "Document Type",
+ editable: false,
+ width: 125,
+ },
+ {
+ name: "Document Name",
+ editable: false,
+ width: 150,
+ },
+ {
+ name: "Reference Date",
+ editable: false,
+ width: 120,
+ },
+ {
+ name: "Amount",
+ editable: false,
+ width: 100,
+ },
+ {
+ name: "Party",
+ editable: false,
+ width: 120,
+ },
+
+ {
+ name: "Reference Number",
+ editable: false,
+ width: 140,
+ },
+ ];
+ }
+
+ get_datatable(proposals_wrapper) {
+ if (!this.datatable) {
+ const datatable_options = {
+ columns: this.columns,
+ data: this.data,
+ dynamicRowHeight: true,
+ checkboxColumn: true,
+ inlineFilters: true,
+ };
+ this.datatable = new frappe.DataTable(
+ proposals_wrapper.get(0),
+ datatable_options
+ );
+ } else {
+ this.datatable.refresh(this.data, this.columns);
+ this.datatable.rowmanager.checkMap = [];
+ }
+ }
+
+ make_dialog() {
+ const me = this;
+ me.selected_payment = null;
+
+ const fields = [
+ {
+ label: __("Action"),
+ fieldname: "action",
+ fieldtype: "Select",
+ options: `Match Against Voucher\nCreate Voucher\nUpdate Bank Transaction`,
+ default: "Match Against Voucher",
+ },
+ {
+ fieldname: "column_break_4",
+ fieldtype: "Column Break",
+ },
+ {
+ label: __("Document Type"),
+ fieldname: "document_type",
+ fieldtype: "Select",
+ options: `Payment Entry\nJournal Entry`,
+ default: "Payment Entry",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_1",
+ label: __("Filters"),
+ depends_on: "eval:doc.action=='Match Against Voucher'",
+ },
+ {
+ fieldtype: "Check",
+ label: "Payment Entry",
+ fieldname: "payment_entry",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Check",
+ label: "Journal Entry",
+ fieldname: "journal_entry",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Sales Invoice",
+ fieldname: "sales_invoice",
+ onchange: () => this.update_options(),
+ },
+
+ {
+ fieldtype: "Check",
+ label: "Purchase Invoice",
+ fieldname: "purchase_invoice",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Expense Claim",
+ fieldname: "expense_claim",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Check",
+ label: "Show Only Exact Amount",
+ fieldname: "exact_match",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_1",
+ label: __("Select Vouchers to Match"),
+ depends_on: "eval:doc.action=='Match Against Voucher'",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "payment_proposals",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "no_matching_vouchers",
+ options: "No Matching Vouchers Found
"
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details",
+ label: "Details",
+ depends_on: "eval:doc.action!='Match Against Voucher'",
+ },
+ {
+ fieldname: "reference_number",
+ fieldtype: "Data",
+ label: "Reference Number",
+ mandatory_depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ default: "Today",
+ fieldname: "posting_date",
+ fieldtype: "Date",
+ label: "Posting Date",
+ reqd: 1,
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "reference_date",
+ fieldtype: "Date",
+ label: "Cheque/Reference Date",
+ mandatory_depends_on: "eval:doc.action=='Create Voucher'",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ reqd: 1,
+ },
+ {
+ fieldname: "mode_of_payment",
+ fieldtype: "Link",
+ label: "Mode of Payment",
+ options: "Mode of Payment",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "edit_in_full_page",
+ fieldtype: "Button",
+ label: "Edit in Full Page",
+ click: () => {
+ this.edit_in_full_page();
+ },
+ depends_on:
+ "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "column_break_7",
+ fieldtype: "Column Break",
+ },
+ {
+ default: "Journal Entry Type",
+ fieldname: "journal_entry_type",
+ fieldtype: "Select",
+ label: "Journal Entry Type",
+ options:
+ "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ },
+ {
+ fieldname: "second_account",
+ fieldtype: "Link",
+ label: "Account",
+ options: "Account",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ get_query: () => {
+ return {
+ filters: {
+ is_group: 0,
+ company: this.company,
+ },
+ };
+ },
+ },
+ {
+ fieldname: "party_type",
+ fieldtype: "Link",
+ label: "Party Type",
+ options: "DocType",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ get_query: function () {
+ return {
+ filters: {
+ name: [
+ "in",
+ Object.keys(frappe.boot.party_account_types),
+ ],
+ },
+ };
+ },
+ },
+ {
+ fieldname: "party",
+ fieldtype: "Dynamic Link",
+ label: "Party",
+ options: "party_type",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldname: "project",
+ fieldtype: "Link",
+ label: "Project",
+ options: "Project",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldname: "cost_center",
+ fieldtype: "Link",
+ label: "Cost Center",
+ options: "Cost Center",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details_section",
+ label: "Transaction Details",
+ collapsible: 1,
+ },
+ {
+ fieldname: "deposit",
+ fieldtype: "Currency",
+ label: "Deposit",
+ read_only: 1,
+ },
+ {
+ fieldname: "withdrawal",
+ fieldtype: "Currency",
+ label: "Withdrawal",
+ read_only: 1,
+ },
+ {
+ fieldname: "description",
+ fieldtype: "Small Text",
+ label: "Description",
+ read_only: 1,
+ },
+ {
+ fieldname: "column_break_17",
+ fieldtype: "Column Break",
+ read_only: 1,
+ },
+ {
+ fieldname: "allocated_amount",
+ fieldtype: "Currency",
+ label: "Allocated Amount",
+ read_only: 1,
+ },
+
+ {
+ fieldname: "unallocated_amount",
+ fieldtype: "Currency",
+ label: "Unallocated Amount",
+ read_only: 1,
+ },
+ ];
+
+ me.dialog = new frappe.ui.Dialog({
+ title: __("Reconcile the Bank Transaction"),
+ fields: fields,
+ size: "large",
+ primary_action: (values) =>
+ this.reconciliation_dialog_primary_action(values),
+ });
+ }
+
+ get_selected_attributes() {
+ let selected_attributes = [];
+ this.dialog.$wrapper.find(".checkbox input").each((i, col) => {
+ if ($(col).is(":checked")) {
+ selected_attributes.push($(col).attr("data-fieldname"));
+ }
+ });
+
+ return selected_attributes;
+ }
+
+ update_options() {
+ let selected_attributes = this.get_selected_attributes();
+ this.get_linked_vouchers(selected_attributes);
+ }
+
+ reconciliation_dialog_primary_action(values) {
+ if (values.action == "Match Against Voucher") this.match(values);
+ if (
+ values.action == "Create Voucher" &&
+ values.document_type == "Payment Entry"
+ )
+ this.add_payment_entry(values);
+ if (
+ values.action == "Create Voucher" &&
+ values.document_type == "Journal Entry"
+ )
+ this.add_journal_entry(values);
+ else if (values.action == "Update Bank Transaction")
+ this.update_transaction(values);
+ }
+
+ match() {
+ var selected_map = this.datatable.rowmanager.checkMap;
+ let rows = [];
+ selected_map.forEach((val, index) => {
+ if (val == 1) rows.push(this.datatable.datamanager.rows[index]);
+ });
+ let vouchers = [];
+ rows.forEach((x) => {
+ vouchers.push({
+ payment_doctype: x[2].content,
+ payment_name: x[3].content,
+ amount: x[5].content,
+ });
+ });
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ vouchers: vouchers,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " Matched";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ add_payment_entry(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ project: values.project,
+ cost_center: values.cost_center,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " added as Payment Entry";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ add_journal_entry(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " added as Journal Entry";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ update_transaction(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_bank_transaction",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ party_type: values.party_type,
+ party: values.party,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " updated";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ edit_in_full_page() {
+ const values = this.dialog.get_values(true);
+ if (values.document_type == "Payment Entry") {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ project: values.project,
+ cost_center: values.cost_center,
+ allow_edit: true
+ },
+ callback: (r) => {
+ const doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ } else {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ allow_edit: true
+ },
+ callback: (r) => {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ }
+ }
+
+};
diff --git a/erpnext/public/js/bank_reconciliation_tool/number_card.js b/erpnext/public/js/bank_reconciliation_tool/number_card.js
new file mode 100644
index 00000000000..e10d109f4d8
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/number_card.js
@@ -0,0 +1,75 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make_cards();
+ }
+
+ make_cards() {
+ this.$reconciliation_tool_cards.empty();
+ this.$cards = [];
+ this.$summary = $(`
`)
+ .hide()
+ .appendTo(this.$reconciliation_tool_cards);
+ var chart_data = [
+ {
+ value: this.bank_statement_closing_balance,
+ label: "Closing Balance as per Bank Statement",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ {
+ value: this.cleared_balance,
+ label: "Closing Balance as per ERP",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ {
+ value:
+ this.bank_statement_closing_balance - this.cleared_balance,
+ label: "Difference",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ ];
+
+ chart_data.forEach((summary) => {
+ let number_card = new erpnext.accounts.NumberCard(summary);
+ this.$cards.push(number_card);
+
+ number_card.$card.appendTo(this.$summary);
+ });
+ this.$cards[2].set_value_color(
+ this.bank_statement_closing_balance - this.cleared_balance == 0
+ ? "text-success"
+ : "text-danger"
+ );
+ this.$summary.css({"border-bottom": "0px", "margin-left": "0px", "margin-right": "0px"});
+ this.$summary.show();
+ }
+};
+
+erpnext.accounts.NumberCard = class NumberCard {
+ constructor(options) {
+ this.$card = frappe.utils.build_summary_item(options);
+ }
+
+ set_value(value) {
+ this.$card.find("div").text(value);
+ }
+
+ set_value_color(color) {
+ this.$card
+ .find("div")
+ .removeClass("text-danger text-success")
+ .addClass(`${color}`);
+ }
+
+ set_indicator(color) {
+ this.$card
+ .find("span")
+ .removeClass("indicator red green")
+ .addClass(`indicator ${color}`);
+ }
+};
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index c96386611bd..67b12fbca4f 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -141,29 +141,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
this.apply_price_list();
},
- price_list_rate: function(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
-
- frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
-
- let item_rate = item.price_list_rate;
- if (doc.doctype == "Purchase Order" && item.blanket_order_rate) {
- item_rate = item.blanket_order_rate;
- }
-
- if (item.discount_percentage) {
- item.discount_amount = flt(item_rate) * flt(item.discount_percentage) / 100;
- }
-
- if (item.discount_amount) {
- item.rate = flt((item.price_list_rate) - (item.discount_amount), precision('rate', item));
- } else {
- item.rate = item_rate;
- }
-
- this.calculate_taxes_and_totals();
- },
-
discount_percentage: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
item.discount_amount = 0.0;
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 416495ceac7..3a3ee3858bf 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -2,7 +2,9 @@
// License: GNU General Public License v3. See license.txt
erpnext.taxes_and_totals = erpnext.payments.extend({
- setup: function() {},
+ setup: function() {
+ this.fetch_round_off_accounts();
+ },
apply_pricing_rule_on_item: function(item) {
let effective_item_rate = item.price_list_rate;
@@ -152,6 +154,24 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
});
},
+ fetch_round_off_accounts: function() {
+ let me = this;
+ frappe.flags.round_off_applicable_accounts = [];
+
+ if (me.frm.doc.company) {
+ return frappe.call({
+ "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
+ "args": {
+ "company": me.frm.doc.company,
+ "account_list": frappe.flags.round_off_applicable_accounts
+ },
+ callback: function(r) {
+ frappe.flags.round_off_applicable_accounts.push(...r.message);
+ }
+ });
+ }
+ },
+
determine_exclusive_rate: function() {
var me = this;
@@ -372,11 +392,21 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
} else if (tax.charge_type == "On Item Quantity") {
current_tax_amount = tax_rate * item.qty;
}
+
+ current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount);
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return current_tax_amount;
},
+ get_final_tax_amount: function(tax, current_tax_amount) {
+ if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
+ current_tax_amount = Math.round(current_tax_amount);
+ }
+
+ return current_tax_amount;
+ },
+
set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail;
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index e5f90490176..1c0abdffcfc 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -649,6 +649,40 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
},
+ price_list_rate: function(doc, cdt, cdn) {
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
+
+ // check if child doctype is Sales Order Item/Qutation Item and calculate the rate
+ if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), cdt)
+ this.apply_pricing_rule_on_item(item);
+ else
+ item.rate = flt(item.price_list_rate * (1 - item.discount_percentage / 100.0),
+ precision("rate", item));
+
+ this.calculate_taxes_and_totals();
+ },
+
+ margin_rate_or_amount: function(doc, cdt, cdn) {
+ // calculated the revised total margin and rate on margin rate changes
+ let item = frappe.get_doc(cdt, cdn);
+ this.apply_pricing_rule_on_item(item);
+ this.calculate_taxes_and_totals();
+ cur_frm.refresh_fields();
+ },
+
+ margin_type: function(doc, cdt, cdn) {
+ // calculate the revised total margin and rate on margin type changes
+ let item = frappe.get_doc(cdt, cdn);
+ if (!item.margin_type) {
+ frappe.model.set_value(cdt, cdn, "margin_rate_or_amount", 0);
+ } else {
+ this.apply_pricing_rule_on_item(item, doc, cdt, cdn);
+ this.calculate_taxes_and_totals();
+ cur_frm.refresh_fields();
+ }
+ },
+
get_incoming_rate: function(item, posting_date, posting_time, voucher_type, company) {
let item_args = {
@@ -1030,7 +1064,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
},
set_margin_amount_based_on_currency: function(exchange_rate) {
- if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]), this.frm.doc.doctype) {
+ if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "Purchase Invoice", "Purchase Order", "Purchase Receipt"]), this.frm.doc.doctype) {
var me = this;
$.each(this.frm.doc.items || [], function(i, d) {
if(d.margin_type == "Amount") {
@@ -1280,10 +1314,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
change_grid_labels: function(company_currency) {
var me = this;
- this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"],
+ this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount", "base_rate_with_margin"],
company_currency, "items");
- this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"],
+ this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate", "rate_with_margin"],
this.frm.doc.currency, "items");
if(this.frm.fields_dict["operations"]) {
@@ -1321,7 +1355,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
// toggle columns
var item_grid = this.frm.fields_dict["items"].grid;
- $.each(["base_rate", "base_price_list_rate", "base_amount"], function(i, fname) {
+ $.each(["base_rate", "base_price_list_rate", "base_amount", "base_rate_with_margin"], function(i, fname) {
if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
@@ -1468,7 +1502,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
});
// if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list
- if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item"]), d.doctype){
+ if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), d.doctype) {
item_list[0]["margin_type"] = d.margin_type;
item_list[0]["margin_rate_or_amount"] = d.margin_rate_or_amount;
}
@@ -1885,7 +1919,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.throw(__("Please enter Item Code to get batch no"));
} else if (doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)) {
-
return {
filters: {'item': item.item_code}
}
@@ -1911,9 +1944,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
set_query_for_item_tax_template: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
if(!item.item_code) {
- frappe.throw(__("Please enter Item Code to get item taxes"));
+ return doc.company ? {filters: {company: doc.company}} : {};
} else {
-
let filters = {
'item_code': item.item_code,
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
@@ -2124,4 +2156,4 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => {
}
}
});
-};
\ No newline at end of file
+};
diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js
index 6cb1207e79e..9548d6c5f36 100644
--- a/erpnext/public/js/telephony.js
+++ b/erpnext/public/js/telephony.js
@@ -4,10 +4,20 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
if (this.df.options == 'Phone') {
this.setup_phone();
}
+ if (this.frm && this.frm.fields_dict) {
+ Object.values(this.frm.fields_dict).forEach(function(field) {
+ if (field.df.read_only === 1 && field.df.options === 'Phone'
+ && field.disp_area.style[0] != 'display' && !field.has_icon) {
+ field.setup_phone();
+ field.has_icon = true;
+ }
+ });
+ }
},
setup_phone() {
if (frappe.phone_call.handler) {
- this.$wrapper.find('.control-input')
+ let control = this.df.read_only ? '.control-value' : '.control-input';
+ this.$wrapper.find(control)
.append(`
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index c39609bd389..e5b50d86eda 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -513,6 +513,7 @@ erpnext.utils.update_child_items = function(opts) {
}, {
fieldtype:'Currency',
fieldname:"rate",
+ options: "currency",
default: 0,
read_only: 0,
in_list_view: 1,
@@ -594,21 +595,7 @@ erpnext.utils.update_child_items = function(opts) {
}
erpnext.utils.map_current_doc = function(opts) {
- let query_args = {};
- if (opts.get_query_filters) {
- query_args.filters = opts.get_query_filters;
- }
-
- if (opts.get_query_method) {
- query_args.query = opts.get_query_method;
- }
-
- if (query_args.filters || query_args.query) {
- opts.get_query = () => {
- return query_args;
- }
- }
- var _map = function() {
+ function _map() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
// remove first item row if empty
if(!cur_frm.doc.items[0].item_code) {
@@ -682,8 +669,22 @@ erpnext.utils.map_current_doc = function(opts) {
}
});
}
- if(opts.source_doctype) {
- var d = new frappe.ui.form.MultiSelectDialog({
+
+ let query_args = {};
+ if (opts.get_query_filters) {
+ query_args.filters = opts.get_query_filters;
+ }
+
+ if (opts.get_query_method) {
+ query_args.query = opts.get_query_method;
+ }
+
+ if (query_args.filters || query_args.query) {
+ opts.get_query = () => query_args;
+ }
+
+ if (opts.source_doctype) {
+ const d = new frappe.ui.form.MultiSelectDialog({
doctype: opts.source_doctype,
target: opts.target,
date_field: opts.date_field || undefined,
@@ -702,7 +703,11 @@ erpnext.utils.map_current_doc = function(opts) {
_map();
},
});
- } else if(opts.source_name) {
+
+ return d;
+ }
+
+ if (opts.source_name) {
opts.source_name = [opts.source_name];
_map();
}
diff --git a/erpnext/quality_management/doctype/non_conformance/non_conformance.json b/erpnext/quality_management/doctype/non_conformance/non_conformance.json
index bfeb96bcaf0..8dfe2d6859d 100644
--- a/erpnext/quality_management/doctype/non_conformance/non_conformance.json
+++ b/erpnext/quality_management/doctype/non_conformance/non_conformance.json
@@ -70,18 +70,18 @@
},
{
"fieldname": "corrective_action",
- "fieldtype": "Text",
+ "fieldtype": "Text Editor",
"label": "Corrective Action"
},
{
"fieldname": "preventive_action",
- "fieldtype": "Text",
+ "fieldtype": "Text Editor",
"label": "Preventive Action"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-26 15:27:47.247814",
+ "modified": "2021-02-26 15:27:47.247814",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Non Conformance",
@@ -115,4 +115,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
index ead403d453a..e2125c3933a 100644
--- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
+++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
@@ -33,8 +33,7 @@
},
{
"fieldname": "sb_00",
- "fieldtype": "Section Break",
- "label": "Agenda"
+ "fieldtype": "Section Break"
},
{
"fieldname": "agenda",
@@ -44,13 +43,12 @@
},
{
"fieldname": "sb_01",
- "fieldtype": "Section Break",
- "label": "Minutes"
+ "fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-27 16:36:45.657883",
+ "modified": "2021-02-27 16:36:45.657883",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Meeting",
@@ -85,4 +83,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json
index 98c33ad33bb..95b930c4c86 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.json
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.json
@@ -1,222 +1,86 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-06-27 15:09:01.318003",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-06-27 15:09:01.318003",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "gst_summary",
+ "column_break_2",
+ "round_off_gst_values",
+ "gstin_email_sent_on",
+ "section_break_4",
+ "gst_accounts",
+ "b2c_limit"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gst_summary",
- "fieldtype": "HTML",
- "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": "GST Summary",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gst_summary",
+ "fieldtype": "HTML",
+ "label": "GST Summary",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gstin_email_sent_on",
- "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": "GSTIN Email Sent On",
- "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,
- "unique": 0
- },
+ "fieldname": "gstin_email_sent_on",
+ "fieldtype": "Date",
+ "label": "GSTIN Email Sent On",
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gst_accounts",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "GST Accounts",
- "length": 0,
- "no_copy": 0,
- "options": "GST Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gst_accounts",
+ "fieldtype": "Table",
+ "label": "GST Accounts",
+ "options": "GST Account",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "250000",
- "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
- "fieldname": "b2c_limit",
- "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": "B2C Limit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "250000",
+ "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
+ "fieldname": "b2c_limit",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "B2C Limit",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "description": "Enabling this option will round off individual GST components in all the Invoices",
+ "fieldname": "round_off_gst_values",
+ "fieldtype": "Check",
+ "label": "Round Off GST Values",
+ "show_days": 1,
+ "show_seconds": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-14 08:14:15.375181",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "GST Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "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
-}
\ No newline at end of file
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-01-28 17:19:47.969260",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "GST Settings",
+ "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/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 8174da20cb7..023b4ed22bc 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -14,8 +14,20 @@ import json
test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"]
class TestGSTR3BReport(unittest.TestCase):
- def test_gstr_3b_report(self):
+ def setUp(self):
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
+ frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
+
+ make_company()
+ make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
+ set_account_heads()
+ make_customers()
+ make_suppliers()
+
+ def test_gstr_3b_report(self):
month_number_mapping = {
1: "January",
2: "February",
@@ -31,17 +43,6 @@ class TestGSTR3BReport(unittest.TestCase):
12: "December"
}
- frappe.set_user("Administrator")
-
- frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
- frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
- frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
-
- make_company()
- make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
- set_account_heads()
- make_customers()
- make_suppliers()
make_sales_invoice()
create_purchase_invoices()
@@ -67,6 +68,42 @@ class TestGSTR3BReport(unittest.TestCase):
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
+ def test_gst_rounding(self):
+ gst_settings = frappe.get_doc('GST Settings')
+ gst_settings.round_off_gst_values = 1
+ gst_settings.save()
+
+ current_country = frappe.flags.country
+ frappe.flags.country = 'India'
+
+ si = create_sales_invoice(company="_Test Company GST",
+ customer = '_Test GST Customer',
+ currency = 'INR',
+ warehouse = 'Finished Goods - _GST',
+ debit_to = 'Debtors - _GST',
+ income_account = 'Sales - _GST',
+ expense_account = 'Cost of Goods Sold - _GST',
+ cost_center = 'Main - _GST',
+ rate=216,
+ do_not_save=1
+ )
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "IGST - _GST",
+ "cost_center": "Main - _GST",
+ "description": "IGST @ 18.0",
+ "rate": 18
+ })
+
+ si.save()
+ # Check for 39 instead of 38.88
+ self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 39)
+
+ frappe.flags.country = current_country
+ gst_settings.round_off_gst_values = 1
+ gst_settings.save()
+
def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
@@ -145,7 +182,6 @@ def make_sales_invoice():
si3.submit()
def create_purchase_invoices():
-
pi = make_purchase_invoice(
company="_Test Company GST",
supplier = '_Test Registered Supplier',
@@ -193,7 +229,6 @@ def create_purchase_invoices():
pi1.submit()
def make_suppliers():
-
if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({
"supplier_group": "_Test Supplier Group",
@@ -257,7 +292,6 @@ def make_suppliers():
address.save()
def make_customers():
-
if not frappe.db.exists("Customer", "_Test GST Customer"):
frappe.get_doc({
"customer_group": "_Test Customer Group",
@@ -354,9 +388,9 @@ def make_customers():
address.save()
def make_company():
-
if frappe.db.exists("Company", "_Test Company GST"):
return
+
company = frappe.new_doc("Company")
company.company_name = "_Test Company GST"
company.abbr = "_GST"
@@ -388,7 +422,6 @@ def make_company():
address.save()
def set_account_heads():
-
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
new file mode 100644
index 00000000000..54cde9c0cf4
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
@@ -0,0 +1,67 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Tax Exemption 80G Certificate', {
+ refresh: function(frm) {
+ if (frm.doc.donor) {
+ frm.set_query('donation', function() {
+ return {
+ filters: {
+ docstatus: 1,
+ donor: frm.doc.donor
+ }
+ };
+ });
+ }
+ },
+
+ recipient: function(frm) {
+ if (frm.doc.recipient === 'Donor') {
+ frm.set_value({
+ 'member': '',
+ 'member_name': '',
+ 'member_email': '',
+ 'member_pan_number': '',
+ 'fiscal_year': '',
+ 'total': 0,
+ 'payments': []
+ });
+ } else {
+ frm.set_value({
+ 'donor': '',
+ 'donor_name': '',
+ 'donor_email': '',
+ 'donor_pan_number': '',
+ 'donation': '',
+ 'date_of_donation': '',
+ 'amount': 0,
+ 'mode_of_payment': '',
+ 'razorpay_payment_id': ''
+ });
+ }
+ },
+
+ get_payments: function(frm) {
+ frm.call({
+ doc: frm.doc,
+ method: 'get_payments',
+ freeze: true
+ });
+ },
+
+ company: function(frm) {
+ if ((frm.doc.member || frm.doc.donor) && frm.doc.company) {
+ frm.call({
+ doc: frm.doc,
+ method: 'set_company_address',
+ freeze: true
+ });
+ }
+ },
+
+ donation: function(frm) {
+ if (frm.doc.recipient === 'Donor' && !frm.doc.donor) {
+ frappe.msgprint(__('Please select donor first'));
+ }
+ }
+});
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
new file mode 100644
index 00000000000..9eee722f420
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
@@ -0,0 +1,297 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2021-02-15 12:37:21.577042",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "recipient",
+ "member",
+ "member_name",
+ "member_email",
+ "member_pan_number",
+ "donor",
+ "donor_name",
+ "donor_email",
+ "donor_pan_number",
+ "column_break_4",
+ "date",
+ "fiscal_year",
+ "section_break_11",
+ "company",
+ "company_address",
+ "company_address_display",
+ "column_break_14",
+ "company_pan_number",
+ "company_80g_number",
+ "company_80g_wef",
+ "title",
+ "section_break_6",
+ "get_payments",
+ "payments",
+ "total",
+ "donation_details_section",
+ "donation",
+ "date_of_donation",
+ "amount",
+ "column_break_27",
+ "mode_of_payment",
+ "razorpay_payment_id"
+ ],
+ "fields": [
+ {
+ "fieldname": "recipient",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Certificate Recipient",
+ "options": "Member\nDonor",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "member",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Member",
+ "mandatory_depends_on": "eval:doc.recipient === \"Member\";",
+ "options": "Member"
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.member_name",
+ "fieldname": "member_name",
+ "fieldtype": "Data",
+ "label": "Member Name",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fieldname": "donor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Donor",
+ "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
+ "options": "Donor"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "payments",
+ "fieldtype": "Table",
+ "label": "Payments",
+ "options": "Tax Exemption 80G Certificate Detail"
+ },
+ {
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "fiscal_year",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Fiscal Year",
+ "options": "Fiscal Year"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "get_payments",
+ "fieldtype": "Button",
+ "label": "Get Memberships"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "NPO-80G-.YYYY.-"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break",
+ "label": "Company Details"
+ },
+ {
+ "fieldname": "company_address",
+ "fieldtype": "Link",
+ "label": "Company Address",
+ "options": "Address"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "company.pan_details",
+ "fieldname": "company_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Number",
+ "read_only": 1
+ },
+ {
+ "fieldname": "company_address_display",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Company Address Display",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "company.company_80g_number",
+ "fieldname": "company_80g_number",
+ "fieldtype": "Data",
+ "label": "80G Number",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "company.with_effect_from",
+ "fieldname": "company_80g_wef",
+ "fieldtype": "Date",
+ "label": "80G With Effect From",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fieldname": "donation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Donation Details"
+ },
+ {
+ "fieldname": "donation",
+ "fieldtype": "Link",
+ "label": "Donation",
+ "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
+ "options": "Donation"
+ },
+ {
+ "fetch_from": "donation.amount",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.mode_of_payment",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.razorpay_payment_id",
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "RazorPay Payment ID",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.date",
+ "fieldname": "date_of_donation",
+ "fieldtype": "Date",
+ "label": "Date of Donation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.donor_name",
+ "fieldname": "donor_name",
+ "fieldtype": "Data",
+ "label": "Donor Name",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.email",
+ "fieldname": "donor_email",
+ "fieldtype": "Data",
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.email_id",
+ "fieldname": "member_email",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.pan_number",
+ "fieldname": "member_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Details",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.pan_number",
+ "fieldname": "donor_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Details",
+ "read_only": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "print_hide": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-22 00:03:34.215633",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Tax Exemption 80G Certificate",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "member, member_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "title",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
new file mode 100644
index 00000000000..d734a18c3ab
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import getdate, flt, get_link_to_form
+from erpnext.accounts.utils import get_fiscal_year
+from frappe.contacts.doctype.address.address import get_company_address
+
+class TaxExemption80GCertificate(Document):
+ def validate(self):
+ self.validate_date()
+ self.validate_duplicates()
+ self.validate_company_details()
+ self.set_company_address()
+ self.set_title()
+
+ def validate_date(self):
+ if self.recipient == 'Member':
+ if getdate(self.date):
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ if not (fiscal_year.year_start_date <= getdate(self.date) \
+ <= fiscal_year.year_end_date):
+ frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
+
+ def validate_duplicates(self):
+ if self.recipient == 'Donor':
+ certificate = frappe.db.exists(self.doctype, {'donation': self.donation})
+ if certificate:
+ frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format(
+ get_link_to_form(self.doctype, certificate), frappe.bold(self.donation)
+ ), title=_('Duplicate Certificate'))
+
+ def validate_company_details(self):
+ fields = ['company_80g_number', 'with_effect_from', 'pan_details']
+ company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True)
+ if not company_details.company_80g_number:
+ frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'),
+ get_link_to_form('Company', self.company)))
+
+ if not company_details.pan_details:
+ frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
+ get_link_to_form('Company', self.company)))
+
+ def set_company_address(self):
+ address = get_company_address(self.company)
+ self.company_address = address.company_address
+ self.company_address_display = address.company_address_display
+
+ def set_title(self):
+ if self.recipient == "Member":
+ self.title = self.member_name
+ else:
+ self.title = self.donor_name
+
+ def get_payments(self):
+ if not self.member:
+ frappe.throw(_('Please select a Member first.'))
+
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ memberships = frappe.db.get_all('Membership', {
+ 'member': self.member,
+ 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
+ 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
+ 'membership_status': ('!=', 'Cancelled')
+ }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'])
+
+ if not memberships:
+ frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
+
+ total = 0
+ self.payments = []
+
+ for doc in memberships:
+ self.append('payments', {
+ 'date': doc.from_date,
+ 'amount': doc.amount,
+ 'invoice_id': doc.invoice,
+ 'razorpay_payment_id': doc.payment_id,
+ 'membership': doc.name
+ })
+ total += flt(doc.amount)
+
+ self.total = total
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
new file mode 100644
index 00000000000..346ebbf6796
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import getdate
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.non_profit.doctype.donation.test_donation import create_donor, create_mode_of_payment, create_donor_type
+from erpnext.non_profit.doctype.donation.donation import create_donation
+from erpnext.non_profit.doctype.membership.test_membership import setup_membership, make_membership
+from erpnext.non_profit.doctype.member.member import create_member
+
+class TestTaxExemption80GCertificate(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql('delete from `tabTax Exemption 80G Certificate`')
+ frappe.db.sql('delete from `tabMembership`')
+ create_donor_type()
+ settings = frappe.get_doc('Non Profit Settings')
+ settings.company = '_Test Company'
+ settings.donation_company = '_Test Company'
+ settings.default_donor_type = '_Test Donor'
+ settings.creation_user = 'Administrator'
+ settings.save()
+
+ company = frappe.get_doc('Company', '_Test Company')
+ company.pan_details = 'BBBTI3374C'
+ company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087'
+ company.with_effect_from = getdate()
+ company.save()
+
+ def test_duplicate_donation_certificate(self):
+ donor = create_donor()
+ create_mode_of_payment()
+ payment = frappe._dict({
+ 'amount': 100,
+ 'method': 'Debit Card',
+ 'id': 'pay_MeXAmsgeKOhq7O'
+ })
+ donation = create_donation(donor, payment)
+
+ args = frappe._dict({
+ 'recipient': 'Donor',
+ 'donor': donor.name,
+ 'donation': donation.name
+ })
+ certificate = create_80g_certificate(args)
+ certificate.insert()
+
+ # check company details
+ self.assertEquals(certificate.company_pan_number, 'BBBTI3374C')
+ self.assertEquals(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087')
+
+ # check donation details
+ self.assertEquals(certificate.amount, donation.amount)
+
+ duplicate_certificate = create_80g_certificate(args)
+ # duplicate validation
+ self.assertRaises(frappe.ValidationError, duplicate_certificate.insert)
+
+ def test_membership_80g_certificate(self):
+ plan = setup_membership()
+
+ # make test member
+ member_doc = create_member(frappe._dict({
+ 'fullname': "_Test_Member",
+ 'email': "_test_member_erpnext@example.com",
+ 'plan_id': plan.name
+ }))
+ member_doc.make_customer_and_link()
+ member = member_doc.name
+
+ membership = make_membership(member, { "from_date": getdate() })
+ invoice = membership.generate_invoice(save=True)
+
+ args = frappe._dict({
+ 'recipient': 'Member',
+ 'member': member,
+ 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name')
+ })
+ certificate = create_80g_certificate(args)
+ certificate.get_payments()
+ certificate.insert()
+
+ self.assertEquals(len(certificate.payments), 1)
+ self.assertEquals(certificate.payments[0].amount, membership.amount)
+ self.assertEquals(certificate.payments[0].invoice_id, invoice.name)
+
+
+def create_80g_certificate(args):
+ certificate = frappe.get_doc({
+ 'doctype': 'Tax Exemption 80G Certificate',
+ 'recipient': args.recipient,
+ 'date': getdate(),
+ 'company': '_Test Company'
+ })
+
+ certificate.update(args)
+
+ return certificate
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
new file mode 100644
index 00000000000..dfa817dd271
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
@@ -0,0 +1,66 @@
+{
+ "actions": [],
+ "creation": "2021-02-15 12:43:52.754124",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "date",
+ "amount",
+ "invoice_id",
+ "column_break_4",
+ "razorpay_payment_id",
+ "membership"
+ ],
+ "fields": [
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "invoice_id",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Invoice ID",
+ "options": "Sales Invoice",
+ "reqd": 1
+ },
+ {
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "Razorpay Payment ID"
+ },
+ {
+ "fieldname": "membership",
+ "fieldtype": "Link",
+ "label": "Membership",
+ "options": "Membership"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-15 16:35:10.777587",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Tax Exemption 80G Certificate Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
new file mode 100644
index 00000000000..bdad798d980
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class TaxExemption80GCertificateDetail(Document):
+ pass
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 9fa94c401f0..7cd64f2fc07 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,12 +1,12 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
- refresh(frm) {
- const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
+ async refresh(frm) {
+ const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
- if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
+ if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@@ -83,7 +83,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Generate E-Way Bill'),
- wide: 1,
+ size: "large",
fields: get_ewaybill_fields(frm),
primary_action: function() {
const data = d.get_values();
@@ -188,7 +188,6 @@ const get_ewaybill_fields = (frm) => {
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
- 'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.vehicle_no
},
{
@@ -253,7 +252,7 @@ const request_irn_generation = (frm) => {
const get_preview_dialog = (frm, action) => {
const dialog = new frappe.ui.Dialog({
title: __("Preview"),
- wide: 1,
+ size: "large",
fields: [
{
"label": "Preview",
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 2043f498251..96f7f1b224f 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -10,6 +10,7 @@ import sys
import json
import base64
import frappe
+import six
import traceback
import io
from frappe import _, bold
@@ -108,11 +109,13 @@ def get_party_details(address_name):
pincode = 999999
return frappe._dict(dict(
- gstin=d.gstin, legal_name=d.address_title,
- location=d.city, pincode=d.pincode,
+ gstin=d.gstin,
+ legal_name=sanitize_for_json(d.address_title),
+ location=sanitize_for_json(d.city),
+ pincode=d.pincode,
state_code=d.gst_state_number,
- address_line1=d.address_line1,
- address_line2=d.address_line2
+ address_line1=sanitize_for_json(d.address_line1),
+ address_line2=sanitize_for_json(d.address_line2)
))
def get_gstin_details(gstin):
@@ -146,8 +149,11 @@ def get_overseas_address_details(address_name):
)
return frappe._dict(dict(
- gstin='URP', legal_name=address_title, location=city,
- address_line1=address_line1, address_line2=address_line2,
+ gstin='URP',
+ legal_name=sanitize_for_json(address_title),
+ location=city,
+ address_line1=sanitize_for_json(address_line1),
+ address_line2=sanitize_for_json(address_line2),
pincode=999999, state_code=96, place_of_supply=96
))
@@ -160,7 +166,7 @@ def get_item_list(invoice):
item.update(d.as_dict())
item.sr_no = d.idx
- item.description = d.item_name.replace('"', '\\"')
+ item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
item.discount_amount = 0
@@ -196,9 +202,11 @@ def update_item_taxes(invoice, item):
item[attr] = 0
for t in invoice.taxes:
- # this contains item wise tax rate & tax amount (incl. discount)
- item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
- if t.account_head in gst_accounts_list:
+ is_applicable = t.tax_amount and t.account_head in gst_accounts_list
+ if is_applicable:
+ # this contains item wise tax rate & tax amount (incl. discount)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
+
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
@@ -223,7 +231,7 @@ def get_invoice_value_details(invoice):
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total)
- invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
else:
invoice_value_details.base_total = abs(invoice.base_net_total)
# since tax already considers discount amount
@@ -326,7 +334,7 @@ def make_einvoice(invoice):
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
- place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
+ place_of_supply = get_place_of_supply(invoice, invoice.doctype) or sanitize_for_json(invoice.billing_address_gstin)
place_of_supply = place_of_supply[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
@@ -356,7 +364,7 @@ def make_einvoice(invoice):
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
- einvoice = json.loads(einvoice)
+ einvoice = safe_json_load(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice)
@@ -371,6 +379,18 @@ def make_einvoice(invoice):
return einvoice
+def safe_json_load(json_string):
+ JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
+
+ try:
+ return json.loads(json_string)
+ except JSONDecodeError as e:
+ # print a snippet of 40 characters around the location where error occured
+ pos = e.pos
+ start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
+ snippet = json_string[start:end]
+ frappe.throw(_("Error in input data. Please check for any special characters near following input: {}").format(snippet))
+
def validate_einvoice(validations, einvoice, errors=[]):
for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None)
@@ -798,6 +818,13 @@ class GSPConnector():
self.invoice.flags.ignore_validate = True
self.invoice.save()
+
+def sanitize_for_json(string):
+ """Escape JSON specific characters from a string."""
+
+ # json.dumps adds double-quotes to the string. Indexing to remove them.
+ return json.dumps(string)[1:-1]
+
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 526198424f3..ee49aae0501 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -21,6 +21,7 @@ def setup_company_independent_fixtures():
add_permissions()
add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
+ create_gratuity_rule()
add_print_formats()
def add_hsn_sac_codes():
@@ -105,8 +106,9 @@ def add_print_formats():
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
- frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
- name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
+ frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -398,9 +400,9 @@ def make_custom_fields(update=True):
si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
-
+
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
-
+
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
@@ -498,6 +500,14 @@ def make_custom_fields(update=True):
fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'),
+ dict(fieldname='non_profit_section', label='Non Profit Settings',
+ fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ dict(fieldname='company_80g_number', label='80G Number',
+ fieldtype='Data', insert_after='non_profit_section'),
+ dict(fieldname='with_effect_from', label='80G With Effect From',
+ fieldtype='Date', insert_after='company_80g_number'),
+ dict(fieldname='pan_details', label='PAN Number',
+ fieldtype='Data', insert_after='with_effect_from')
],
'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption',
@@ -580,7 +590,15 @@ def make_custom_fields(update=True):
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
],
- "Member": [
+ 'Member': [
+ {
+ 'fieldname': 'pan_number',
+ 'label': 'PAN Details',
+ 'fieldtype': 'Data',
+ 'insert_after': 'email_id'
+ }
+ ],
+ 'Donor': [
{
'fieldname': 'pan_number',
'label': 'PAN Details',
@@ -642,7 +660,7 @@ def set_tax_withholding_category(company):
pass
docs = get_tds_details(accounts, fiscal_year)
-
+
for d in docs:
try:
doc = frappe.get_doc(d)
@@ -660,7 +678,7 @@ def set_tax_withholding_category(company):
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
if not fy_exist:
doc.append("rates", d.get('rates')[0])
-
+
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.save()
@@ -822,4 +840,24 @@ def get_tds_details(accounts, fiscal_year):
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}])
- ]
\ No newline at end of file
+ ]
+
+def create_gratuity_rule():
+
+ # Standard Indain Gratuity Rule
+ if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
+ rule = frappe.new_doc("Gratuity Rule")
+ rule.name = "Indian Standard Gratuity Rule"
+ rule.calculate_gratuity_amount_based_on = "Current Slab"
+ rule.work_experience_calculation_method = "Round Off Work Experience"
+ rule.minimum_year_for_gratuity = 5
+
+ fraction = 15/26
+ rule.append("gratuity_rule_slabs", {
+ "from_year": 0,
+ "to_year":0,
+ "fraction_of_applicable_earnings": fraction
+ })
+
+ rule.flags.ignore_mandatory = True
+ rule.save()
\ No newline at end of file
diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py
new file mode 100644
index 00000000000..7ce27f6cf5a
--- /dev/null
+++ b/erpnext/regional/india/test_utils.py
@@ -0,0 +1,38 @@
+from __future__ import unicode_literals
+
+import unittest
+import frappe
+from unittest.mock import patch
+from erpnext.regional.india.utils import validate_document_name
+
+
+class TestIndiaUtils(unittest.TestCase):
+ @patch("frappe.get_cached_value")
+ def test_validate_document_name(self, mock_get_cached):
+ mock_get_cached.return_value = "India" # mock country
+ posting_date = "2021-05-01"
+
+ invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",
+ "SI.2020.0001", "PI2021 - 001" ]
+ for name in invalid_names:
+ doc = frappe._dict(name=name, posting_date=posting_date)
+ self.assertRaises(frappe.ValidationError, validate_document_name, doc)
+
+ valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001",
+ "2020-PI-0001", "PI2020-0001" ]
+ for name in valid_names:
+ doc = frappe._dict(name=name, posting_date=posting_date)
+ try:
+ validate_document_name(doc)
+ except frappe.ValidationError:
+ self.fail("Valid name {} throwing error".format(name))
+
+ @patch("frappe.get_cached_value")
+ def test_validate_document_name_not_india(self, mock_get_cached):
+ mock_get_cached.return_value = "Not India"
+ doc = frappe._dict(name="SI$123", posting_date="2021-05-01")
+
+ try:
+ validate_document_name(doc)
+ except frappe.ValidationError:
+ self.fail("Regional validation related to India are being applied to other countries")
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index e89885f3805..1a618d6cf56 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe, re, json
from frappe import _
import erpnext
-from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
+from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@@ -14,6 +14,13 @@ from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.utils import get_account_currency
from frappe.model.utils import get_fetch_values
+
+GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - /
+GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
+GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
+PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
+
+
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state]
@@ -37,12 +44,10 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
if gst_category and gst_category == 'UIN Holders':
- p = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
- if not p.match(doc.gstin):
+ if not GSTIN_UIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
else:
- p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
- if not p.match(doc.gstin):
+ if not GSTIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
validate_gstin_check_digit(doc.gstin)
@@ -55,6 +60,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_pan_for_india(doc, method):
+ if doc.get('country') != 'India' or not doc.pan:
+ return
+
+ if not PAN_NUMBER_FORMAT.match(doc.pan):
+ frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
+
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:
@@ -140,6 +152,20 @@ def get_itemised_tax_breakup_data(doc, account_wise=False):
def set_place_of_supply(doc, method=None):
doc.place_of_supply = get_place_of_supply(doc, doc.doctype)
+def validate_document_name(doc, method=None):
+ """Validate GST invoice number requirements."""
+ country = frappe.get_cached_value("Company", doc.company, "country")
+
+ # Date was chosen as start of next FY to avoid irritating current users.
+ if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"):
+ return
+
+ if len(doc.name) > 16:
+ frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series."))
+
+ if not GST_INVOICE_NUMBER_FORMAT.match(doc.name):
+ frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series."))
+
# don't remove this function it is used in tests
def test_method():
'''test function'''
@@ -772,3 +798,24 @@ def make_regional_gl_entries(gl_entries, doc):
)
return gl_entries
+
+@frappe.whitelist()
+def get_regional_round_off_accounts(company, account_list):
+ country = frappe.get_cached_value('Company', company, 'country')
+
+ if country != 'India':
+ return
+
+ if isinstance(account_list, string_types):
+ account_list = json.loads(account_list)
+
+ if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'):
+ return
+
+ gst_accounts = get_gst_accounts(company)
+ gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ + gst_accounts.get('igst_account')
+
+ account_list.extend(gst_account_list)
+
+ return account_list
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
index 217d623a8d5..95b92e76a69 100644
--- a/erpnext/regional/italy/setup.py
+++ b/erpnext/regional/italy/setup.py
@@ -189,9 +189,7 @@ def make_custom_fields(update=True):
def setup_report():
report_name = 'Electronic Invoice Register'
-
- frappe.db.sql(""" update `tabReport` set disabled = 0 where
- name = %s """, report_name)
+ frappe.db.set_value("Report", report_name, "disabled", 0)
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
new file mode 100644
index 00000000000..a8da0bd2097
--- /dev/null
+++ b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
@@ -0,0 +1,26 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-02-22 00:17:33.878581",
+ "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Tax Exemption 80G Certificate",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "{% if letter_head and not no_letterhead -%}\n {{ letter_head }}
\n{%- endif %}\n\n\n
{{ doc.company }} 80G Donor Certificate \n\n \n\n\n
{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n
\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }} \n
\n
\n \n
\n\n This is to confirm that the {{ doc.company }} received an amount of
{{doc.get_formatted(\"amount\")}} \n from
{{ doc.donor_name }} \n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n
\n \n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n
\n
\n\n \n {{doc.company_address_display }}
\n\n",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2021-02-22 00:20:08.516600",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "80G Certificate for Donation",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
new file mode 100644
index 00000000000..f1b15aab298
--- /dev/null
+++ b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
@@ -0,0 +1,26 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-02-15 16:53:55.026611",
+ "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Tax Exemption 80G Certificate",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "{% if letter_head and not no_letterhead -%}\n {{ letter_head }}
\n{%- endif %}\n\n\n
{{ doc.company }} Members 80G Donor Certificate \n Financial Cycle {{ doc.fiscal_year }} \n\n \n\n\n
{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n
\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }} \n
\n
\n \n
\n This is to confirm that the {{ doc.company }} received a total amount of
{{doc.get_formatted(\"total\")}} \n from
{{ doc.member_name }} \n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n as per the payment details given below:\n \n
\n
\n \t\n \t\t\n \t\t\t{{ _(\"Date\") }} \n \t\t\t{{ _(\"Amount\") }} \n \t\t\t{{ _(\"Invoice ID\") }} \n \t\t \n \t \n \t\n \t\t{%- for payment in doc.payments -%}\n \t\t\n \t\t\t {{ payment.date }} \n \t\t\t{{ payment.get_formatted(\"amount\") }} \n \t\t\t{{ payment.invoice_id }} \n \t\t \n \t\t{%- endfor -%}\n \t \n
\n \n
\n \n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n
\n
\n\n \n {{doc.company_address_display }}
\n\n",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2021-02-21 23:29:00.778973",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "80G Certificate for Membership",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 96dc3f728d9..09b04ff367f 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -236,6 +236,7 @@ class Gstr1Report(object):
self.cgst_sgst_invoices = []
unidentified_gst_accounts = []
+ unidentified_gst_accounts_invoice = []
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if account in self.gst_accounts.cess_account:
self.invoice_cess.setdefault(parent, tax_amount)
@@ -251,6 +252,7 @@ class Gstr1Report(object):
if not (cgst_or_sgst or account in self.gst_accounts.igst_account):
if "gst" in account.lower() and account not in unidentified_gst_accounts:
unidentified_gst_accounts.append(account)
+ unidentified_gst_accounts_invoice.append(parent)
continue
for item_code, tax_amounts in item_wise_tax_detail.items():
@@ -273,7 +275,7 @@ class Gstr1Report(object):
# Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items):
- if invoice not in self.items_based_on_tax_rate \
+ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py
index 776a82c7306..68208ab31bf 100644
--- a/erpnext/regional/united_arab_emirates/setup.py
+++ b/erpnext/regional/united_arab_emirates/setup.py
@@ -7,12 +7,15 @@ 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
+from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule
def setup(company=None, patch=True):
make_custom_fields()
add_print_formats()
add_custom_roles_for_reports()
add_permissions()
+ create_gratuity_rule()
+
if company:
create_sales_tax(company)
@@ -155,3 +158,93 @@ def add_permissions():
add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1)
+
+def create_gratuity_rule():
+ rule_1 = rule_2 = rule_3 = None
+
+ # Rule Under Limited Contract
+ slabs = get_slab_for_limited_contract()
+ if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"):
+ rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs")
+
+ # Rule Under Unlimited Contract on termination
+ slabs = get_slab_for_unlimited_contract_on_termination()
+ if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)"):
+ rule_2 = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)", slabs)
+
+ # Rule Under Unlimited Contract on resignation
+ slabs = get_slab_for_unlimited_contract_on_resignation()
+ if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"):
+ rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs)
+
+ #for applicable salary component user need to set this by its own
+ if rule_1:
+ rule_1.flags.ignore_mandatory = True
+ rule_1.save()
+ if rule_2:
+ rule_2.flags.ignore_mandatory = True
+ rule_2.save()
+ if rule_3:
+ rule_3.flags.ignore_mandatory = True
+ rule_3.save()
+
+
+def get_slab_for_limited_contract():
+ return [{
+ "from_year": 0,
+ "to_year":1,
+ "fraction_of_applicable_earnings": 0
+ },
+ {
+ "from_year": 1,
+ "to_year":5,
+ "fraction_of_applicable_earnings": 21/30
+ },
+ {
+ "from_year": 5,
+ "to_year":0,
+ "fraction_of_applicable_earnings": 1
+ }]
+
+def get_slab_for_unlimited_contract_on_termination():
+ return [{
+ "from_year": 0,
+ "to_year":1,
+ "fraction_of_applicable_earnings": 0
+ },
+ {
+ "from_year": 1,
+ "to_year":5,
+ "fraction_of_applicable_earnings": 21/30
+ },
+ {
+ "from_year": 5,
+ "to_year":0,
+ "fraction_of_applicable_earnings": 1
+ }]
+
+def get_slab_for_unlimited_contract_on_resignation():
+ fraction_1 = 1/3 * 21/30
+ fraction_2 = 2/3 * 21/30
+ fraction_3 = 21/30
+
+ return [{
+ "from_year": 0,
+ "to_year":1,
+ "fraction_of_applicable_earnings": 0
+ },
+ {
+ "from_year": 1,
+ "to_year":3,
+ "fraction_of_applicable_earnings": fraction_1
+ },
+ {
+ "from_year": 3,
+ "to_year":5,
+ "fraction_of_applicable_earnings": fraction_2
+ },
+ {
+ "from_year": 5,
+ "to_year":0,
+ "fraction_of_applicable_earnings": fraction_3
+ }]
diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py
index 2b0ecafebc5..24ab1cf049f 100644
--- a/erpnext/regional/united_states/setup.py
+++ b/erpnext/regional/united_states/setup.py
@@ -36,5 +36,4 @@ def make_custom_fields(update=True):
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
- frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
- name in('IRS 1099 Form') """)
+ frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 557c7151d96..7d5e84df52f 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -16,6 +16,8 @@
"customer_name",
"gender",
"customer_type",
+ "pan",
+ "tax_withholding_category",
"default_bank_account",
"lead_name",
"image",
@@ -34,9 +36,8 @@
"companies",
"currency_and_price_list",
"default_currency",
- "default_price_list",
"column_break_14",
- "language",
+ "default_price_list",
"address_contacts",
"address_html",
"website",
@@ -59,6 +60,7 @@
"column_break_45",
"market_segment",
"industry",
+ "language",
"is_frozen",
"column_break_38",
"loyalty_program",
@@ -479,13 +481,25 @@
"fieldname": "dn_required",
"fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note"
+ },
+ {
+ "fieldname": "pan",
+ "fieldtype": "Data",
+ "label": "PAN"
+ },
+ {
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category"
}
],
"icon": "fa fa-user",
"idx": 363,
"image_field": "image",
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-03-17 11:03:42.706907",
+ "modified": "2021-01-28 12:54:57.258959",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 87fdaa366f1..7761aa70fb2 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -54,7 +54,11 @@ class TestCustomer(unittest.TestCase):
details = get_party_details("_Test Customer")
for key, value in iteritems(to_check):
- self.assertEqual(value, details.get(key))
+ val = details.get(key)
+ if not val and not isinstance(val, list):
+ val = None
+
+ self.assertEqual(value, val)
def test_party_details_tax_category(self):
from erpnext.accounts.party import get_party_details
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index a6785f709a1..8b53902d32f 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -641,6 +641,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -648,7 +649,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:39:40.174551",
+ "modified": "2021-02-23 01:13:54.670763",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 52a0174798e..ee16f441715 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -555,12 +555,12 @@ class TestSalesOrder(unittest.TestCase):
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", {
- "item_tax_template": "Test Update Items Template",
+ "item_tax_template": "Test Update Items Template - _TC",
"valid_from": nowdate()
})
new_item_with_tax.save()
- tax_template = "_Test Account Excise Duty @ 10"
+ tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
@@ -614,7 +614,7 @@ class TestSalesOrder(unittest.TestCase):
so.cancel()
so.delete()
new_item_with_tax.delete()
- frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
def test_warehouse_user(self):
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 37e47a9d410..1e5590e7489 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -786,6 +786,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -793,7 +794,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:35:07.617320",
+ "modified": "2021-02-23 01:15:05.803091",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 4044f09c855..2104c0131c4 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -140,7 +140,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 12:12:56.784014",
+ "modified": "2021-03-02 17:35:53.603607",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@@ -157,5 +157,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 338a3ccf24c..278821e3928 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -93,6 +93,10 @@ erpnext.PointOfSale.Controller = class {
})
return frappe.utils.play_sound("error");
}
+
+ // filter balance details for empty rows
+ balance_details = balance_details.filter(d => d.mode_of_payment);
+
const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher";
const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true });
!res.exc && me.prepare_app_defaults(res.message);
@@ -498,10 +502,11 @@ erpnext.PointOfSale.Controller = class {
async on_cart_update(args) {
frappe.dom.freeze();
+ let item_row = undefined;
try {
let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom } = item;
- let item_row = this.get_item_from_frm(item_code, batch_no, uom);
+ item_row = this.get_item_from_frm(item_code, batch_no, uom);
const item_selected_from_selector = field === 'qty' && value === "+1"
@@ -553,10 +558,12 @@ erpnext.PointOfSale.Controller = class {
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row);
}
+
} catch (error) {
console.log(error);
} finally {
frappe.dom.unfreeze();
+ return item_row;
}
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 044e80357dc..9ab9eefa30d 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -472,7 +472,8 @@ erpnext.PointOfSale.ItemCart = class {
if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total);
- this.render_grand_total(frm.doc.grand_total);
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
+ this.render_grand_total(grand_total);
const taxes = frm.doc.taxes.map(t => {
return {
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 7c116e9fa17..e0d5b731665 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -152,6 +152,10 @@ erpnext.PointOfSale.ItemSelector = class {
this.item_group_field.toggle_label(false);
}
+ set_search_value(value) {
+ $(this.search_field.$input[0]).val(value).trigger("input");
+ }
+
bind_events() {
const me = this;
window.onScan = onScan;
@@ -159,7 +163,7 @@ erpnext.PointOfSale.ItemSelector = class {
onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) {
this.search_field.set_focus();
- $(this.search_field.$input[0]).val(sScancode).trigger("input");
+ this.set_search_value(sScancode);
this.barcode_scanned = true;
}
}
@@ -178,6 +182,7 @@ erpnext.PointOfSale.ItemSelector = class {
uom = uom === "undefined" ? undefined : uom;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
+ me.set_search_value('');
});
this.search_field.$input.on('input', (e) => {
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index bcbac3b4be8..22a279d463f 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -223,7 +223,8 @@ erpnext.PointOfSale.Payment = class {
if (success) {
title = __("Payment Received");
- if (amount >= doc.grand_total) {
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
+ if (amount >= grand_total) {
frappe.dom.unfreeze();
message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
this.events.submit_invoice();
@@ -243,7 +244,8 @@ erpnext.PointOfSale.Payment = class {
auto_set_remaining_amount() {
const doc = this.events.get_frm().doc;
- const remaining_amount = doc.grand_total - doc.paid_amount;
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
+ const remaining_amount = grand_total - doc.paid_amount;
const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
if (!current_value && remaining_amount > 0 && this.selected_mode) {
this.selected_mode.set_value(remaining_amount);
@@ -389,7 +391,7 @@ erpnext.PointOfSale.Payment = class {
}
attach_cash_shortcuts(doc) {
- const grand_total = doc.grand_total;
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
const currency = doc.currency;
const shortcuts = this.get_cash_shortcuts(flt(grand_total));
@@ -499,7 +501,8 @@ erpnext.PointOfSale.Payment = class {
update_totals_section(doc) {
if (!doc) doc = this.events.get_frm().doc;
const paid_amount = doc.paid_amount;
- const remaining = doc.grand_total - doc.paid_amount;
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
+ const remaining = grand_total - doc.paid_amount;
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
const currency = doc.currency;
const label = change ? __('Change') : __('To Be Paid');
@@ -507,7 +510,7 @@ erpnext.PointOfSale.Payment = class {
this.$totals.html(
`
Grand Total
-
${format_currency(doc.grand_total, currency)}
+
${format_currency(grand_total, currency)}
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index ce084646e15..04285735abd 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -127,20 +127,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
this.set_dynamic_labels();
},
- price_list_rate: function(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
- frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
-
- // check if child doctype is Sales Order Item/Qutation Item and calculate the rate
- if(in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item"]), cdt)
- this.apply_pricing_rule_on_item(item);
- else
- item.rate = flt(item.price_list_rate * (1 - item.discount_percentage / 100.0),
- precision("rate", item));
-
- this.calculate_taxes_and_totals();
- },
-
discount_percentage: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
item.discount_amount = 0.0;
@@ -353,26 +339,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
refresh_field('product_bundle_help');
},
- margin_rate_or_amount: function(doc, cdt, cdn) {
- // calculated the revised total margin and rate on margin rate changes
- var item = locals[cdt][cdn];
- this.apply_pricing_rule_on_item(item)
- this.calculate_taxes_and_totals();
- cur_frm.refresh_fields();
- },
-
- margin_type: function(doc, cdt, cdn){
- // calculate the revised total margin and rate on margin type changes
- var item = locals[cdt][cdn];
- if(!item.margin_type) {
- frappe.model.set_value(cdt, cdn, "margin_rate_or_amount", 0);
- } else {
- this.apply_pricing_rule_on_item(item, doc,cdt, cdn)
- this.calculate_taxes_and_totals();
- cur_frm.refresh_fields();
- }
- },
-
company_address: function() {
var me = this;
if(this.frm.doc.company_address) {
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 36033d9daee..c041d269a76 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -140,7 +140,7 @@ frappe.ui.form.on("Company", {
doc: frm.doc,
freeze: true,
callback: function() {
- frappe.msgprint(__("Default tax templates for sales and purchase are created."));
+ frappe.msgprint(__("Default tax templates for sales, purchase and items are created."));
}
})
},
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index d49ae7ce8ac..56f60dfcff0 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -725,7 +725,7 @@
{
"fieldname": "default_in_transit_warehouse",
"fieldtype": "Link",
- "label": "Default In Transit Warehouse",
+ "label": "Default In-Transit Warehouse",
"options": "Warehouse"
},
{
@@ -740,7 +740,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2020-12-03 12:27:27.085094",
+ "modified": "2021-02-16 15:53:37.167589",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 819ba78e666..433851cde53 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -390,8 +390,10 @@ class Company(NestedSet):
frappe.db.sql("delete from tabDepartment where company=%s", self.name)
frappe.db.sql("delete from `tabTax Withholding Account` where company=%s", self.name)
+ # delete tax templates
frappe.db.sql("delete from `tabSales Taxes and Charges Template` where company=%s", self.name)
frappe.db.sql("delete from `tabPurchase Taxes and Charges Template` where company=%s", self.name)
+ frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name)
@frappe.whitelist()
def enqueue_replace_abbr(company, old, new):
diff --git a/erpnext/setup/doctype/company/company_list.js b/erpnext/setup/doctype/company/company_list.js
index 017286560fe..1d1184f04d3 100644
--- a/erpnext/setup/doctype/company/company_list.js
+++ b/erpnext/setup/doctype/company/company_list.js
@@ -1,10 +1,5 @@
frappe.listview_settings['Company'] = {
- onload: () => {
- frappe.breadcrumbs.add({
- type: 'Custom',
- module: __('Accounts'),
- label: __('Accounts'),
- route: '#modules/Accounts'
- });
- }
-}
\ No newline at end of file
+ onload() {
+ frappe.breadcrumbs.add('Accounts');
+ },
+};
diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py
index 7a72fe31023..0df4c87f51f 100644
--- a/erpnext/setup/doctype/company/delete_company_transactions.py
+++ b/erpnext/setup/doctype/company/delete_company_transactions.py
@@ -28,7 +28,7 @@ def delete_company_transactions(company_name):
"Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
- "Item Default", "Customer", "Supplier"):
+ "Item Default", "Customer", "Supplier", "GST Account"):
delete_for_doctype(doctype, company_name)
# reset company values
diff --git a/erpnext/setup/doctype/item_group/test_records.json b/erpnext/setup/doctype/item_group/test_records.json
index 71159643209..146da87bddc 100644
--- a/erpnext/setup/doctype/item_group/test_records.json
+++ b/erpnext/setup/doctype/item_group/test_records.json
@@ -79,13 +79,13 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"tax_category": ""
},
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 12",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"tax_category": "_Test Tax Category 1"
}
]
@@ -99,7 +99,7 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 15",
+ "item_tax_template": "_Test Account Excise Duty @ 15 - _TC",
"tax_category": ""
}
]
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 72ed00293ed..5053c6a5124 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -195,6 +195,7 @@ def install(country=None):
{'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"},
+ {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"},
{'doctype': "Opportunity Type", "name": "Hub"},
{'doctype': "Opportunity Type", "name": _("Sales")},
diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index e66fa76f93a..c3c1593c046 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -29,6 +29,7 @@ def make_tax_account_and_template(company, account_name, tax_rate, template_name
try:
if accounts:
make_sales_and_purchase_tax_templates(accounts, template_name)
+ make_item_tax_templates(accounts, template_name)
except frappe.NameError:
if frappe.message_log: frappe.message_log.pop()
except RootNotEditable:
@@ -84,6 +85,27 @@ def make_sales_and_purchase_tax_templates(accounts, template_name=None):
doc = frappe.get_doc(purchase_tax_template)
doc.insert(ignore_permissions=True)
+def make_item_tax_templates(accounts, template_name=None):
+ if not template_name:
+ template_name = accounts[0].name
+
+ item_tax_template = {
+ "doctype": "Item Tax Template",
+ "title": template_name,
+ "company": accounts[0].company,
+ 'taxes': []
+ }
+
+
+ for account in accounts:
+ item_tax_template['taxes'].append({
+ "tax_type": account.name,
+ "tax_rate": account.tax_rate
+ })
+
+ # Items
+ frappe.get_doc(copy.deepcopy(item_tax_template)).insert(ignore_permissions=True)
+
def get_tax_account_group(company):
tax_group = frappe.db.get_value("Account",
{"account_name": "Duties and Taxes", "is_group": 1, "company": company})
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
index 36917213022..7a4bb20136f 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
@@ -190,7 +190,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-02-11 18:48:30.433058",
+ "modified": "2021-03-02 17:34:57.642565",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Settings",
@@ -207,5 +207,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py
index b3ae804b1c2..283f7d5fdaf 100644
--- a/erpnext/stock/__init__.py
+++ b/erpnext/stock/__init__.py
@@ -38,7 +38,7 @@ def get_warehouse_account_map(company=None):
frappe.flags.warehouse_account_map[company] = warehouse_account
else:
frappe.flags.warehouse_account_map = warehouse_account
-
+
return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map
def get_warehouse_account(warehouse, warehouse_account=None):
@@ -64,10 +64,14 @@ def get_warehouse_account(warehouse, warehouse_account=None):
if not account and warehouse.company:
account = get_company_default_inventory_account(warehouse.company)
+ if not account and warehouse.company:
+ account = frappe.db.get_value('Account',
+ {'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name')
+
if not account and warehouse.company and not warehouse.is_group:
frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company))
return account
def get_company_default_inventory_account(company):
- return frappe.get_cached_value('Company', company, 'default_inventory_account')
+ return frappe.get_cached_value('Company', company, 'default_inventory_account')
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index c8424f13e12..8fdda565d20 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -93,7 +93,7 @@ class Batch(Document):
if create_new_batch:
if batch_number_series:
- self.batch_id = make_autoname(batch_number_series)
+ self.batch_id = make_autoname(batch_number_series, doc=self)
elif batch_uses_naming_series():
self.batch_id = self.get_name_from_naming_series()
else:
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 1088b4127d3..0514bd23942 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -16,8 +16,9 @@ class Bin(Document):
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
+
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
@@ -34,11 +35,13 @@ class Bin(Document):
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
- "sle_id": args.name
+ "sle_id": args.name,
+ "creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
- # Validate negative qty in future transactions
- validate_negative_qty_in_future_sle(args)
+ # update qty in future ale and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
def update_qty(self, args):
# update the stock values (for current quantities)
@@ -51,7 +54,7 @@ class Bin(Document):
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
+
self.set_projected_qty()
self.db_update()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 559f8be0dea..d39b22965e3 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase):
def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status
- dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
+ make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
+
+ dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
dn.submit()
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 17996247c55..b05090a237e 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -750,6 +750,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -758,7 +759,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:42:03.767968",
+ "modified": "2021-02-23 01:04:08.588104",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index f851aafd9ad..55391235cb0 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -85,7 +85,7 @@ frappe.ui.form.on("Item", {
}
if (frm.doc.variant_of) {
frm.set_intro(__('This Item is a Variant of {0} (Template).',
- [`
${frm.doc.variant_of} `]), true);
+ [`
${frm.doc.variant_of} `]), true);
}
if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) {
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index baddcaf024a..33a8fe7c8d8 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -521,8 +521,7 @@
"fieldname": "has_variants",
"fieldtype": "Check",
"in_standard_filter": 1,
- "label": "Has Variants",
- "no_copy": 1
+ "label": "Has Variants"
},
{
"default": "Item Attribute",
@@ -538,7 +537,6 @@
"fieldtype": "Table",
"hidden": 1,
"label": "Attributes",
- "no_copy": 1,
"options": "Item Variant Attribute"
},
{
@@ -1068,7 +1066,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
- "modified": "2021-02-18 13:41:04.108932",
+ "modified": "2021-03-15 13:41:04.108932",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index b661570fd77..7b7d2da969c 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -177,7 +177,7 @@ class Item(WebsiteGenerator):
if not self.valuation_rate and self.standard_rate:
self.valuation_rate = self.standard_rate
- if not self.valuation_rate:
+ if not self.valuation_rate and not self.is_customer_provided_item:
frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered"))
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 109731abb53..36d0de1e5df 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -104,41 +104,41 @@ class TestItem(unittest.TestCase):
def test_item_tax_template(self):
expected_item_tax_template = [
{"item_code": "_Test Item With Item Tax Template", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 10"},
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"},
{"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12"},
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
{"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
{"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 10"},
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12"},
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
{"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 15"},
+ "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12"},
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
{"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 20"},
+ "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"},
{"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Item Tax Template 1"},
+ "item_tax_template": "_Test Item Tax Template 1 - _TC"},
{"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
]
expected_item_tax_map = {
None: {},
- "_Test Account Excise Duty @ 10": {"_Test Account Excise Duty - _TC": 10},
- "_Test Account Excise Duty @ 12": {"_Test Account Excise Duty - _TC": 12},
- "_Test Account Excise Duty @ 15": {"_Test Account Excise Duty - _TC": 15},
- "_Test Account Excise Duty @ 20": {"_Test Account Excise Duty - _TC": 20},
- "_Test Item Tax Template 1": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10,
+ "_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10},
+ "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12},
+ "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15},
+ "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20},
+ "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10,
"_Test Account S&H Education Cess - _TC": 15}
}
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 8f437b13f0d..909c4eeb906 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -92,7 +92,7 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 10"
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"
}
],
"stock_uom": "_Test UOM 1"
@@ -370,12 +370,12 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 10"
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"
},
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 12",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"tax_category": "_Test Tax Category 1"
}
]
@@ -449,13 +449,13 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 20"
+ "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"
},
{
"doctype": "Item Tax",
"parentfield": "taxes",
"tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Item Tax Template 1"
+ "item_tax_template": "_Test Item Tax Template 1 - _TC"
}
]
},
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 471e6853b51..9b1a47eed6c 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
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"specification",
+ "parameter_group",
"value",
"numeric",
"column_break_3",
@@ -75,12 +76,20 @@
"in_list_view": 1,
"label": "Numeric",
"width": "80px"
+ },
+ {
+ "fetch_from": "specification.parameter_group",
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-01 19:18:46.924399",
+ "modified": "2021-02-04 18:50:02.056173",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 550c849c5d1..70687bdac26 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -295,7 +295,8 @@ class PurchaseReceipt(BuyingController):
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(amount["base_amount"]),
+ "credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
+ account_currency!=self.company_currency) else flt(amount["amount"])),
"credit_in_account_currency": flt(amount["amount"]),
"project": d.project
}, item=d))
@@ -323,10 +324,12 @@ class PurchaseReceipt(BuyingController):
else:
loss_account = self.get_company_default("default_expense_account")
+ cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center")
+
gl_entries.append(self.get_gl_dict({
"account": loss_account,
"against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
+ "cost_center": cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": divisional_loss,
"project": d.project
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index ca58ab28236..7741ee7f609 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase):
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, "stock_value")
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
+
+ if existing_bin_qty < 0:
+ make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
pr = make_purchase_receipt()
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 8974ad9318d..efe3642d23c 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -37,10 +37,16 @@
"returned_qty",
"rate_and_amount",
"price_list_rate",
- "discount_percentage",
- "discount_amount",
"col_break3",
"base_price_list_rate",
+ "discount_and_margin_section",
+ "margin_type",
+ "margin_rate_or_amount",
+ "rate_with_margin",
+ "column_break_37",
+ "discount_percentage",
+ "discount_amount",
+ "base_rate_with_margin",
"sec_break1",
"rate",
"amount",
@@ -880,6 +886,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
},
@@ -890,12 +897,55 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "discount_and_margin_section",
+ "fieldtype": "Section Break",
+ "label": "Discount and Margin"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "margin_type",
+ "fieldtype": "Select",
+ "label": "Margin Type",
+ "options": "\nPercentage\nAmount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate",
+ "fieldname": "margin_rate_or_amount",
+ "fieldtype": "Float",
+ "label": "Margin Rate or Amount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_37",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "base_rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:44:06.918515",
+ "modified": "2021-02-23 00:59:14.360847",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
index 0b5a9b5b3ce..418b4825f2f 100644
--- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
@@ -7,24 +7,34 @@
"engine": "InnoDB",
"field_order": [
"parameter",
+ "parameter_group",
"description"
],
"fields": [
{
"fieldname": "parameter",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "Parameter",
+ "reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
+ },
+ {
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-28 18:06:54.897317",
+ "modified": "2021-02-19 20:33:30.657406",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js
new file mode 100644
index 00000000000..8716a298716
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Quality Inspection Parameter Group', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json
new file mode 100644
index 00000000000..57264741a64
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json
@@ -0,0 +1,82 @@
+{
+ "actions": [],
+ "autoname": "field:group_name",
+ "creation": "2021-02-04 18:44:12.223295",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "group_name"
+ ],
+ "fields": [
+ {
+ "fieldname": "group_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Parameter Group Name",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-04 18:44:12.223295",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Quality Inspection Parameter Group",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py
new file mode 100644
index 00000000000..1a3b1a04639
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class QualityInspectionParameterGroup(Document):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py
new file mode 100644
index 00000000000..212d4b8c21b
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestQualityInspectionParameterGroup(unittest.TestCase):
+ pass
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 35d58eff58b..0eff5a8f003 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"specification",
+ "parameter_group",
"status",
"value",
"numeric",
@@ -210,12 +211,20 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Numeric"
+ },
+ {
+ "fetch_from": "specification.parameter_group",
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-01 19:46:22.138018",
+ "modified": "2021-02-04 19:15:37.991221",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index f22c6018d41..8436acbed25 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -46,6 +46,9 @@ class RepostItemValuation(Document):
def repost(doc):
try:
+ if not frappe.db.exists("Repost Item Valuation", doc.name):
+ return
+
doc.set_status('In Progress')
frappe.db.commit()
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 6bacf1f8a33..c8d8ca9e17e 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -554,7 +554,7 @@ def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch
if batch_nos:
try:
- filters["batch_no"] = json.loads(batch_nos)
+ filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
except:
filters["batch_no"] = [batch_nos]
@@ -626,4 +626,4 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]):
batch_no_condition=batch_no_condition
), filters, as_dict=1)
- return serial_numbers
\ No newline at end of file
+ return serial_numbers
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 726118d06d1..64dcbed1d85 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -231,15 +231,37 @@ frappe.ui.form.on('Stock Entry', {
}, __("Get Items From"));
frm.add_custom_button(__('Material Request'), function() {
- erpnext.utils.map_current_doc({
+ const allowed_request_types = ["Material Transfer", "Material Issue", "Customer Provided"];
+ const depends_on_condition = "eval:doc.material_request_type==='Customer Provided'";
+ const d = erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.material_request.material_request.make_stock_entry",
source_doctype: "Material Request",
target: frm,
date_field: "schedule_date",
- setters: {},
+ setters: [{
+ fieldtype: 'Select',
+ label: __('Purpose'),
+ options: allowed_request_types.join("\n"),
+ fieldname: 'material_request_type',
+ default: "Material Transfer",
+ mandatory: 1,
+ change() {
+ if (this.value === 'Customer Provided') {
+ d.dialog.get_field("customer").set_focus();
+ }
+ },
+ },
+ {
+ fieldtype: 'Link',
+ label: __('Customer'),
+ options: 'Customer',
+ fieldname: 'customer',
+ depends_on: depends_on_condition,
+ mandatory_depends_on: depends_on_condition,
+ }],
get_query_filters: {
docstatus: 1,
- material_request_type: ["in", ["Material Transfer", "Material Issue"]],
+ material_request_type: ["in", allowed_request_types],
status: ["not in", ["Transferred", "Issued"]]
}
})
@@ -569,6 +591,7 @@ frappe.ui.form.on('Stock Entry', {
add_to_transit: function(frm) {
if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') {
+ frm.set_value('to_warehouse', '');
frm.set_value('stock_entry_type', 'Material Transfer');
frm.fields_dict.to_warehouse.get_query = function() {
return {
@@ -579,7 +602,15 @@ frappe.ui.form.on('Stock Entry', {
}
};
};
- frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => {
+ frm.trigger('set_tansit_warehouse');
+ }
+ },
+
+ set_tansit_warehouse: function(frm) {
+ if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse) {
+ let dt = frm.doc.from_warehouse ? 'Warehouse' : 'Company';
+ let dn = frm.doc.from_warehouse ? frm.doc.from_warehouse : frm.doc.company;
+ frappe.db.get_value(dt, dn, 'default_in_transit_warehouse', (r) => {
if (r.default_in_transit_warehouse) {
frm.set_value('to_warehouse', r.default_in_transit_warehouse);
}
@@ -946,6 +977,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
},
from_warehouse: function(doc) {
+ this.frm.trigger('set_tansit_warehouse');
this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse);
},
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d77b70ff145..ea1b3873ea7 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -163,7 +163,7 @@ class StockEntry(StockController):
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
- if self.job_card and self.purpose != 'Material Transfer for Manufacture':
+ if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']:
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card))
@@ -276,9 +276,10 @@ class StockEntry(StockController):
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
for item_code, qty_list in iteritems(item_wise_qty):
- if self.fg_completed_qty != sum(qty_list):
+ total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty"))
+ if self.fg_completed_qty != total:
frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different")
- .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty)))
+ .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty)))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -823,6 +824,7 @@ class StockEntry(StockController):
if self.job_card:
job_doc = frappe.get_doc('Job Card', self.job_card)
job_doc.set_transferred_qty(update_status=True)
+ job_doc.set_transferred_qty_in_job_card(self)
if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order)
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 988ae929691..864ff488b22 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -69,7 +69,8 @@
"putaway_rule",
"column_break_51",
"reference_purchase_receipt",
- "quality_inspection"
+ "quality_inspection",
+ "job_card_item"
],
"fields": [
{
@@ -532,13 +533,22 @@
"fieldname": "is_finished_item",
"fieldtype": "Check",
"label": "Is Finished Item"
+ },
+ {
+ "fieldname": "job_card_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Job Card Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-30 15:00:44.489442",
+ "modified": "2021-02-11 13:47:50.158754",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 36d09efd1ad..b0e7440e6cc 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -38,6 +38,7 @@ class StockLedgerEntry(Document):
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
+
def on_submit(self):
self.check_stock_frozen_date()
self.actual_amt_check()
diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js
index 1bea00e2632..1f172504a7f 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.js
+++ b/erpnext/stock/doctype/warehouse/warehouse.js
@@ -3,6 +3,18 @@
frappe.ui.form.on("Warehouse", {
+ onload: function(frm) {
+ frm.set_query("default_in_transit_warehouse", function() {
+ return {
+ filters:{
+ 'warehouse_type' : 'Transit',
+ 'is_group': 0,
+ 'company': frm.doc.company
+ }
+ };
+ });
+ },
+
refresh: function(frm) {
frm.toggle_display('warehouse_name', frm.doc.__islocal);
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 1cc600b9ca7..bddb114c9de 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -13,6 +13,7 @@
"column_break_3",
"warehouse_type",
"parent_warehouse",
+ "default_in_transit_warehouse",
"is_group",
"column_break_4",
"account",
@@ -230,13 +231,20 @@
{
"fieldname": "column_break_3",
"fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "eval: doc.warehouse_type !== 'Transit';",
+ "fieldname": "default_in_transit_warehouse",
+ "fieldtype": "Link",
+ "label": "Default In-Transit Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-building",
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-08-03 18:41:52.442502",
+ "modified": "2021-02-16 17:21:52.380098",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index 6f12c2731bb..fe2417bba7e 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -82,11 +82,6 @@ frappe.query_reports["Stock Ledger"] = {
"label": __("Include UOM"),
"fieldtype": "Link",
"options": "UOM"
- },
- {
- "fieldname": "show_cancelled_entries",
- "label": __("Show Cancelled Entries"),
- "fieldtype": "Check"
}
],
"formatter": function (value, row, column, data, default_formatter) {
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 7b5701a9932..36996e96745 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -138,7 +138,7 @@ def get_stock_ledger_entries(filters, items):
`tabStock Ledger Entry` sle
WHERE
company = %(company)s
- AND posting_date BETWEEN %(from_date)s AND %(to_date)s
+ AND is_cancelled = 0 AND posting_date BETWEEN %(from_date)s AND %(to_date)s
{sle_conditions}
{item_conditions_sql}
ORDER BY
@@ -209,9 +209,6 @@ def get_sle_conditions(filters):
if filters.get("project"):
conditions.append("project=%(project)s")
- if not filters.get("show_cancelled_entries"):
- conditions.append("is_cancelled = 0")
-
return "and {}".format(" and ".join(conditions)) if conditions else ""
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 21860b6863f..f54b3c1bb20 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -23,6 +23,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
cancel = sl_entries[0].get("is_cancelled")
if cancel:
+ validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
@@ -45,6 +46,21 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+def validate_cancellation(args):
+ if args[0].get("is_cancelled"):
+ repost_entry = frappe.db.get_value("Repost Item Valuation", {
+ 'voucher_type': args[0].voucher_type,
+ 'voucher_no': args[0].voucher_no,
+ 'docstatus': 1
+ }, ['name', 'status'], as_dict=1)
+
+ if repost_entry:
+ if repost_entry.status == 'In Progress':
+ frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
+ if repost_entry.status == 'Queued':
+ doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+ doc.cancel()
+ doc.delete()
def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
@@ -74,7 +90,8 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
"item_code": args[i].item_code,
"warehouse": args[i].warehouse,
"posting_date": args[i].posting_date,
- "posting_time": args[i].posting_time
+ "posting_time": args[i].posting_time,
+ "creation": args[i].get("creation")
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
for item_wh, new_sle in iteritems(obj.new_items):
@@ -86,7 +103,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
def get_args_for_voucher(voucher_type, voucher_no):
return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
- fields=["item_code", "warehouse", "posting_date", "posting_time"],
+ fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse"
)
@@ -155,7 +172,7 @@ class update_entries_after(object):
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
- previous_sle = self.get_sle_before_datetime(args)
+ previous_sle = self.get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -167,9 +184,35 @@ class update_entries_after(object):
"stock_value_difference": 0.0
})
+ def get_previous_sle_of_current_voucher(self, args):
+ """get stock ledger entries filtered by specific posting datetime conditions"""
+
+ args['time_format'] = '%H:%i:%s'
+ if not args.get("posting_date"):
+ args["posting_date"] = "1900-01-01"
+ if not args.get("posting_time"):
+ args["posting_time"] = "00:00"
+
+ sle = frappe.db.sql("""
+ select *, timestamp(posting_date, posting_time) as "timestamp"
+ from `tabStock Ledger Entry`
+ where item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and is_cancelled = 0
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ order by timestamp(posting_date, posting_time) desc, creation desc
+ limit 1""", args, as_dict=1)
+
+ return sle[0] if sle else frappe._dict()
+
+
def build(self):
+ from erpnext.controllers.stock_controller import check_if_future_sle_exists
+
if self.args.get("sle_id"):
- self.process_sle_against_current_voucher()
+ self.process_sle_against_current_timestamp()
+ if not check_if_future_sle_exists(self.args):
+ self.update_bin()
else:
entries_to_fix = self.get_future_entries_to_fix()
@@ -183,12 +226,12 @@ class update_entries_after(object):
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
+ self.update_bin()
+
if self.exceptions:
self.raise_exceptions()
- self.update_bin()
-
- def process_sle_against_current_voucher(self):
+ def process_sle_against_current_timestamp(self):
sl_entries = self.get_sle_against_current_voucher()
for sle in sl_entries:
self.process_sle(sle)
@@ -204,8 +247,8 @@ class update_entries_after(object):
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
- and voucher_type = %(voucher_type)s
- and voucher_no = %(voucher_no)s
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+
order by
creation ASC
for update
@@ -232,7 +275,6 @@ class update_entries_after(object):
return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
-
self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle \
@@ -398,7 +440,7 @@ class update_entries_after(object):
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
- doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no)
+ doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
doc.update_valuation_rate(reset_outgoing_rate=False)
for d in (doc.items + doc.supplied_items):
d.db_update()
@@ -639,7 +681,6 @@ class update_entries_after(object):
# update bin for each warehouse
for warehouse, data in iteritems(self.data):
bin_doc = get_bin(self.item_code, warehouse)
-
bin_doc.update({
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
@@ -765,6 +806,25 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
return valuation_rate
+def update_qty_in_future_sle(args, allow_negative_stock=None):
+ frappe.db.sql("""
+ update `tabStock Ledger Entry`
+ set qty_after_transaction = qty_after_transaction + {qty}
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ """.format(qty=args.actual_qty), args)
+
+ validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
@@ -793,7 +853,7 @@ def get_future_sle_with_negative_qty(args):
and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and is_cancelled = 0
- and qty_after_transaction + {0} < 0
+ and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
limit 1
- """.format(args.actual_qty), args, as_dict=1)
+ """, args, as_dict=1)
\ No newline at end of file
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 167e80fa39c..9fe12f9490b 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -49,8 +49,8 @@ frappe.ui.form.on("Issue", {
},
refresh: function (frm) {
- if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
- if (frm.doc.service_level_agreement) {
+ if (frm.doc.status !== "Closed") {
+ if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") {
frappe.call({
"method": "frappe.client.get",
args: {
diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py
index 00f66e72c3e..26f59a2395e 100644
--- a/erpnext/www/lms/index.py
+++ b/erpnext/www/lms/index.py
@@ -13,4 +13,4 @@ def get_context(context):
def get_featured_programs():
- return utils.get_portal_programs()
\ No newline at end of file
+ return utils.get_portal_programs() or []
\ No newline at end of file
diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py
index d3b04c2f8f6..104d3fa315a 100644
--- a/erpnext/www/lms/program.py
+++ b/erpnext/www/lms/program.py
@@ -26,4 +26,4 @@ def get_program(program_name):
def get_course_progress(courses, program):
progress = {course.name: utils.get_course_progress(course, program) for course in courses}
- return progress
\ No newline at end of file
+ return progress or {}
\ No newline at end of file