diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 0a72d4fa4e6..c6de6410ebc 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -89,7 +89,7 @@ class Account(NestedSet):
throw(_("Root cannot be edited."), RootNotEditable)
if not self.parent_account and not self.is_group:
- frappe.throw(_("Root Account must be a group"))
+ frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index dc23b2b2d05..89bb0184af8 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -69,6 +69,7 @@ class TestAccount(unittest.TestCase):
acc.account_name = "Accumulated Depreciation"
acc.parent_account = "Fixed Assets - _TC"
acc.company = "_Test Company"
+ acc.account_type = "Accumulated Depreciation"
acc.insert()
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
@@ -149,7 +150,7 @@ def _make_test_records(verbose):
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
- ["_Test Accumulated Depreciations", "Current Assets", 0, None, None],
+ ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 14fdffc0a78..7a85bfb26b6 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -206,12 +206,13 @@ def get_dimension_filters():
WHERE disabled = 0
""", as_dict=1)
- default_dimensions = frappe.db.sql("""SELECT parent, company, default_dimension
- FROM `tabAccounting Dimension Detail`""", as_dict=1)
+ default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension
+ FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
+ WHERE c.parent = p.name""", as_dict=1)
default_dimensions_map = {}
for dimension in default_dimensions:
- default_dimensions_map.setdefault(dimension['company'], {})
- default_dimensions_map[dimension['company']][dimension['parent']] = dimension['default_dimension']
+ default_dimensions_map.setdefault(dimension.company, {})
+ default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
return dimension_filters, default_dimensions_map
diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py
index 084514cbfa7..d93b6ffbaf9 100644
--- a/erpnext/accounts/doctype/budget/budget.py
+++ b/erpnext/accounts/doctype/budget/budget.py
@@ -9,6 +9,7 @@ from frappe.utils import flt, getdate, add_months, get_last_day, fmt_money, nowd
from frappe.model.naming import make_autoname
from erpnext.accounts.utils import get_fiscal_year
from frappe.model.document import Document
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class BudgetError(frappe.ValidationError): pass
class DuplicateBudgetError(frappe.ValidationError): pass
@@ -98,30 +99,32 @@ def validate_expense_against_budget(args):
if not (args.get('account') and args.get('cost_center')) and args.item_code:
args.cost_center, args.account = get_item_details(args)
- if not (args.cost_center or args.project) and not args.account:
+ if not args.account:
return
- for budget_against in ['project', 'cost_center']:
+ for budget_against in ['project', 'cost_center'] + get_accounting_dimensions():
if (args.get(budget_against) and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})):
- if args.project and budget_against == 'project':
- condition = "and b.project=%s" % frappe.db.escape(args.project)
- args.budget_against_field = "Project"
+ doctype = frappe.unscrub(budget_against)
- elif args.cost_center and budget_against == 'cost_center':
- cc_lft, cc_rgt = frappe.db.get_value("Cost Center", args.cost_center, ["lft", "rgt"])
- condition = """and exists(select name from `tabCost Center`
- where lft<=%s and rgt>=%s and name=b.cost_center)""" % (cc_lft, cc_rgt)
- args.budget_against_field = "Cost Center"
+ if frappe.get_cached_value('DocType', doctype, 'is_tree'):
+ lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
+ condition = """and exists(select name from `tab%s`
+ where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec
+ args.is_tree = True
+ else:
+ condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
+ args.is_tree = False
- args.budget_against = args.get(budget_against)
+ args.budget_against_field = budget_against
+ args.budget_against_doctype = doctype
budget_records = frappe.db.sql("""
select
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request,
- ifnull(applicable_on_purchase_order,0) as for_purchase_order,
+ ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
@@ -132,9 +135,7 @@ def validate_expense_against_budget(args):
b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1
{condition}
- """.format(condition=condition,
- budget_against_field=frappe.scrub(args.get("budget_against_field"))),
- (args.fiscal_year, args.account), as_dict=True)
+ """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec
if budget_records:
validate_budget_records(args, budget_records)
@@ -230,10 +231,10 @@ def get_ordered_amount(args, budget):
def get_other_condition(args, budget, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
- budget_against_field = frappe.scrub(args.get("budget_against_field"))
+ budget_against_field = args.get("budget_against_field")
if budget_against_field and args.get(budget_against_field):
- condition += " and child.%s = '%s'" %(budget_against_field, args.get(budget_against_field))
+ condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
if args.get('fiscal_year'):
date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date'
@@ -246,19 +247,30 @@ def get_other_condition(args, budget, for_doc):
return condition
def get_actual_expense(args):
+ if not args.budget_against_doctype:
+ args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
+
+ budget_against_field = args.get('budget_against_field')
condition1 = " and gle.posting_date <= %(month_end_date)s" \
if args.get("month_end_date") else ""
- if args.budget_against_field == "Cost Center":
- lft_rgt = frappe.db.get_value(args.budget_against_field,
- args.budget_against, ["lft", "rgt"], as_dict=1)
+
+ if args.is_tree:
+ lft_rgt = frappe.db.get_value(args.budget_against_doctype,
+ args.get(budget_against_field), ["lft", "rgt"], as_dict=1)
+
args.update(lft_rgt)
- condition2 = """and exists(select name from `tabCost Center`
- where lft>=%(lft)s and rgt<=%(rgt)s and name=gle.cost_center)"""
- elif args.budget_against_field == "Project":
- condition2 = "and exists(select name from `tabProject` where name=gle.project and gle.project = %(budget_against)s)"
+ condition2 = """and exists(select name from `tab{doctype}`
+ where lft>=%(lft)s and rgt<=%(rgt)s
+ and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec
+ budget_against_field=budget_against_field)
+ else:
+ condition2 = """and exists(select name from `tab{doctype}`
+ where name=gle.{budget_against} and
+ gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype,
+ budget_against = budget_against_field)
- return flt(frappe.db.sql("""
+ amount = flt(frappe.db.sql("""
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where gle.account=%(account)s
@@ -267,7 +279,9 @@ def get_actual_expense(args):
and gle.company=%(company)s
and gle.docstatus=1
{condition2}
- """.format(condition1=condition1, condition2=condition2), (args))[0][0])
+ """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec
+
+ return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py
index 33aefd67d1c..9c19791d29a 100644
--- a/erpnext/accounts/doctype/budget/test_budget.py
+++ b/erpnext/accounts/doctype/budget/test_budget.py
@@ -13,7 +13,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self):
- set_total_expense_zero("2013-02-28", "Cost Center")
+ set_total_expense_zero("2013-02-28", "cost_center")
budget = make_budget(budget_against="Cost Center")
@@ -26,7 +26,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_crossed_stop1(self):
- set_total_expense_zero("2013-02-28", "Cost Center")
+ set_total_expense_zero("2013-02-28", "cost_center")
budget = make_budget(budget_against="Cost Center")
@@ -41,7 +41,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_exception_approver_role(self):
- set_total_expense_zero("2013-02-28", "Cost Center")
+ set_total_expense_zero("2013-02-28", "cost_center")
budget = make_budget(budget_against="Cost Center")
@@ -114,7 +114,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_crossed_stop2(self):
- set_total_expense_zero("2013-02-28", "Project")
+ set_total_expense_zero("2013-02-28", "project")
budget = make_budget(budget_against="Project")
@@ -129,7 +129,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_yearly_budget_crossed_stop1(self):
- set_total_expense_zero("2013-02-28", "Cost Center")
+ set_total_expense_zero("2013-02-28", "cost_center")
budget = make_budget(budget_against="Cost Center")
@@ -141,7 +141,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_yearly_budget_crossed_stop2(self):
- set_total_expense_zero("2013-02-28", "Project")
+ set_total_expense_zero("2013-02-28", "project")
budget = make_budget(budget_against="Project")
@@ -153,7 +153,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_on_cancellation1(self):
- set_total_expense_zero("2013-02-28", "Cost Center")
+ set_total_expense_zero("2013-02-28", "cost_center")
budget = make_budget(budget_against="Cost Center")
@@ -177,7 +177,7 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_on_cancellation2(self):
- set_total_expense_zero("2013-02-28", "Project")
+ set_total_expense_zero("2013-02-28", "project")
budget = make_budget(budget_against="Project")
@@ -201,8 +201,8 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_against_group_cost_center(self):
- set_total_expense_zero("2013-02-28", "Cost Center")
- set_total_expense_zero("2013-02-28", "Cost Center", "_Test Cost Center 2 - _TC")
+ set_total_expense_zero("2013-02-28", "cost_center")
+ set_total_expense_zero("2013-02-28", "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
@@ -241,25 +241,30 @@ class TestBudget(unittest.TestCase):
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
- if budget_against_field == "Project":
+ if budget_against_field == "project":
budget_against = "_Test Project"
else:
budget_against = budget_against_CC or "_Test Cost Center - _TC"
- existing_expense = get_actual_expense(frappe._dict({
+
+ args = frappe._dict({
"account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
"monthly_end_date": posting_date,
"company": "_Test Company",
"fiscal_year": "_Test Fiscal Year 2013",
"budget_against_field": budget_against_field,
- "budget_against": budget_against
- }))
+ })
+
+ if not args.get(budget_against_field):
+ args[budget_against_field] = budget_against
+
+ existing_expense = get_actual_expense(args)
if existing_expense:
- if budget_against_field == "Cost Center":
+ if budget_against_field == "cost_center":
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True)
- elif budget_against_field == "Project":
+ elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date="2013-02-28")
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js
index 96ec57dcb0b..9e2f6eed3b6 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.js
+++ b/erpnext/accounts/doctype/cost_center/cost_center.js
@@ -18,7 +18,7 @@ frappe.ui.form.on('Cost Center', {
},
refresh: function(frm) {
if (!frm.is_new()) {
- frm.add_custom_button(__('Update Cost Center Number'), function () {
+ frm.add_custom_button(__('Update Cost Center Name / Number'), function () {
frm.trigger("update_cost_center_number");
});
}
@@ -47,35 +47,45 @@ frappe.ui.form.on('Cost Center', {
},
update_cost_center_number: function(frm) {
var d = new frappe.ui.Dialog({
- title: __('Update Cost Center Number'),
+ title: __('Update Cost Center Name / Number'),
fields: [
{
- "label": 'Cost Center Number',
+ "label": "Cost Center Name",
+ "fieldname": "cost_center_name",
+ "fieldtype": "Data",
+ "reqd": 1,
+ "default": frm.doc.cost_center_name
+ },
+ {
+ "label": "Cost Center Number",
"fieldname": "cost_center_number",
"fieldtype": "Data",
- "reqd": 1
+ "reqd": 1,
+ "default": frm.doc.cost_center_number
}
],
primary_action: function() {
var data = d.get_values();
- if(data.cost_center_number === frm.doc.cost_center_number) {
+ if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) {
d.hide();
return;
}
+ frappe.dom.freeze();
frappe.call({
- method: "erpnext.accounts.utils.update_number_field",
+ method: "erpnext.accounts.utils.update_cost_center",
args: {
- doctype_name: frm.doc.doctype,
- name: frm.doc.name,
- field_name: d.fields[0].fieldname,
- number_value: data.cost_center_number,
+ docname: frm.doc.name,
+ cost_center_name: data.cost_center_name,
+ cost_center_number: data.cost_center_number,
company: frm.doc.company
},
callback: function(r) {
+ frappe.dom.unfreeze();
if(!r.exc) {
if(r.message) {
frappe.set_route("Form", "Cost Center", r.message);
} else {
+ me.frm.set_value("cost_center_name", data.cost_center_name);
me.frm.set_value("cost_center_number", data.cost_center_number);
}
d.hide();
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json
index 126aa543185..5013c92a327 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.json
+++ b/erpnext/accounts/doctype/cost_center/cost_center.json
@@ -2,7 +2,6 @@
"actions": [],
"allow_copy": 1,
"allow_import": 1,
- "allow_rename": 1,
"creation": "2013-01-23 19:57:17",
"description": "Track separate Income and Expense for product verticals or divisions.",
"doctype": "DocType",
@@ -126,7 +125,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-03-18 18:26:01.540170",
+ "modified": "2020-04-29 16:09:30.025214",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 1a530c75395..f367f952b8a 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -561,20 +561,20 @@ class JournalEntry(AccountsController):
if self.write_off_based_on == 'Accounts Receivable':
jd1.party_type = "Customer"
- jd1.credit = flt(d.outstanding_amount, self.precision("credit", "accounts"))
+ jd1.credit_in_account_currency = flt(d.outstanding_amount, self.precision("credit", "accounts"))
jd1.reference_type = "Sales Invoice"
jd1.reference_name = cstr(d.name)
elif self.write_off_based_on == 'Accounts Payable':
jd1.party_type = "Supplier"
- jd1.debit = flt(d.outstanding_amount, self.precision("debit", "accounts"))
+ jd1.debit_in_account_currency = flt(d.outstanding_amount, self.precision("debit", "accounts"))
jd1.reference_type = "Purchase Invoice"
jd1.reference_name = cstr(d.name)
jd2 = self.append('accounts', {})
if self.write_off_based_on == 'Accounts Receivable':
- jd2.debit = total
+ jd2.debit_in_account_currency = total
elif self.write_off_based_on == 'Accounts Payable':
- jd2.credit = total
+ jd2.credit_in_account_currency = total
self.validate_total_debit_and_credit()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index fbf11bca5de..74be4c4ef0f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -104,6 +104,21 @@ frappe.ui.form.on('Payment Entry', {
};
});
+ frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
+ const child = locals[cdt][cdn];
+ if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
+ let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
+
+ payment_term_list = payment_term_list.map(pt => pt.payment_term);
+
+ return {
+ filters: {
+ 'name': ['in', payment_term_list]
+ }
+ }
+ }
+ });
+
frm.set_query("reference_name", "references", function(doc, cdt, cdn) {
const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company};
@@ -272,7 +287,7 @@ frappe.ui.form.on('Payment Entry', {
frm.set_value("contact_email", "");
frm.set_value("contact_person", "");
}
- if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party) {
+ if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
if(!frm.doc.posting_date) {
frappe.msgprint(__("Please select Posting Date before selecting Party"))
frm.set_value("party", "");
@@ -1018,4 +1033,4 @@ frappe.ui.form.on('Payment Entry', {
});
}
},
-})
+})
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c89742cfb7f..3b23aebd00f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
self.set_remarks()
self.validate_duplicate_entry()
self.validate_allocated_amount()
+ self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
self.set_status()
@@ -71,9 +72,9 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
+ self.update_payment_schedule()
self.set_status()
-
def on_cancel(self):
self.setup_party_account_field()
self.make_gl_entries(cancel=1)
@@ -81,6 +82,7 @@ class PaymentEntry(AccountsController):
self.update_advance_paid()
self.update_expense_claim()
self.delink_advance_entry_references()
+ self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
self.set_status()
@@ -94,10 +96,10 @@ class PaymentEntry(AccountsController):
def validate_duplicate_entry(self):
reference_names = []
for d in self.get("references"):
- if (d.reference_doctype, d.reference_name) in reference_names:
+ if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}")
.format(d.idx, d.reference_doctype, d.reference_name))
- reference_names.append((d.reference_doctype, d.reference_name))
+ reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
def set_bank_account_data(self):
if self.bank_account:
@@ -264,6 +266,25 @@ class PaymentEntry(AccountsController):
frappe.throw(_("{0} {1} must be submitted")
.format(d.reference_doctype, d.reference_name))
+ def validate_paid_invoices(self):
+ no_oustanding_refs = {}
+
+ for d in self.get("references"):
+ if not d.allocated_amount:
+ continue
+
+ if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"):
+ outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"])
+ if outstanding_amount <= 0 and not is_return:
+ no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
+
+ for k, v in no_oustanding_refs.items():
+ frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.
\
+ If this is undesirable please cancel the corresponding Payment Entry.")
+ .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")),
+ title=_("Warning"), indicator="orange")
+
+
def validate_journal_entry(self):
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype == "Journal Entry":
@@ -285,6 +306,36 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry")
.format(d.reference_name, dr_or_cr))
+ def update_payment_schedule(self, cancel=0):
+ invoice_payment_amount_map = {}
+ invoice_paid_amount_map = {}
+
+ for reference in self.get('references'):
+ if reference.payment_term and reference.reference_name:
+ key = (reference.payment_term, reference.reference_name)
+ invoice_payment_amount_map.setdefault(key, 0.0)
+ invoice_payment_amount_map[key] += reference.allocated_amount
+
+ if not invoice_paid_amount_map.get(reference.reference_name):
+ payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name},
+ fields=['paid_amount', 'payment_amount', 'payment_term'])
+ for term in payment_schedule:
+ invoice_key = (term.payment_term, reference.reference_name)
+ invoice_paid_amount_map.setdefault(invoice_key, {})
+ invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount
+
+ for key, amount in iteritems(invoice_payment_amount_map):
+ if cancel:
+ frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s
+ WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
+ else:
+ outstanding = invoice_paid_amount_map.get(key)['outstanding']
+ if amount > outstanding:
+ frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
+
+ frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
+ WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
+
def set_status(self):
if self.docstatus == 2:
self.status = 'Cancelled'
@@ -1012,15 +1063,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
frappe.msgprint(_('{0} is on hold till {1}'.format(doc.name, doc.release_date)))
else:
- pe.append("references", {
- 'reference_doctype': dt,
- 'reference_name': dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': grand_total,
- 'outstanding_amount': outstanding_amount,
- 'allocated_amount': outstanding_amount
- })
+ if (doc.doctype in ('Sales Invoice', 'Purchase Invoice')
+ and frappe.get_value('Payment Terms Template',
+ {'name': doc.payment_terms_template}, 'allocate_payment_based_on_payment_terms')):
+
+ for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
+ pe.append('references', reference)
+ else:
+ pe.append("references", {
+ 'reference_doctype': dt,
+ 'reference_name': dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ 'total_amount': grand_total,
+ 'outstanding_amount': outstanding_amount,
+ 'allocated_amount': outstanding_amount
+ })
pe.setup_party_account_field()
pe.set_missing_values()
@@ -1029,6 +1087,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.set_amounts()
return pe
+def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
+ references = []
+ for payment_term in payment_schedule:
+ references.append({
+ 'reference_doctype': dt,
+ 'reference_name': dn,
+ 'bill_no': doc.get('bill_no'),
+ 'due_date': doc.get('due_date'),
+ 'total_amount': grand_total,
+ 'outstanding_amount': outstanding_amount,
+ 'payment_term': payment_term.payment_term,
+ 'allocated_amount': flt(payment_term.payment_amount - payment_term.paid_amount,
+ payment_term.precision('payment_amount'))
+ })
+
+ return references
def get_paid_amount(dt, dn, party_type, party, account, due_date):
if party_type=="Customer":
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js
new file mode 100644
index 00000000000..7ea60bb48ed
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings['Payment Entry'] = {
+
+ onload: function(listview) {
+ listview.page.fields_dict.party_type.get_query = function() {
+ return {
+ "filters": {
+ "name": ["in", Object.keys(frappe.boot.party_account_types)],
+ }
+ };
+ };
+ }
+};
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index a25e0e32c86..756cc8ec547 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -149,6 +149,30 @@ class TestPaymentEntry(unittest.TestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
+ def test_payment_entry_against_payment_terms(self):
+ si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+ create_payment_terms_template()
+ si.payment_terms_template = 'Test Receivable Template'
+
+ si.append('taxes', {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18
+ })
+ si.save()
+
+ si.submit()
+
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ pe.submit()
+ si.load_from_db()
+
+ self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable')
+ self.assertEqual(pe.references[1].payment_term, 'Tax Receivable')
+ self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
+ self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_against_sales_invoice_to_check_status(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
@@ -609,4 +633,38 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(expected_party_account_balance, party_account_balance)
accounts_settings.allow_cost_center_in_entry_of_bs_account = 0
- accounts_settings.save()
\ No newline at end of file
+ accounts_settings.save()
+
+def create_payment_terms_template():
+
+ create_payment_term('Basic Amount Receivable')
+ create_payment_term('Tax Receivable')
+
+ if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'):
+ payment_term_template = frappe.get_doc({
+ 'doctype': 'Payment Terms Template',
+ 'template_name': 'Test Receivable Template',
+ 'allocate_payment_based_on_payment_terms': 1,
+ 'terms': [{
+ 'doctype': 'Payment Terms Template Detail',
+ 'payment_term': 'Basic Amount Receivable',
+ 'invoice_portion': 84.746,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 1
+ },
+ {
+ 'doctype': 'Payment Terms Template Detail',
+ 'payment_term': 'Tax Receivable',
+ 'invoice_portion': 15.254,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 2
+ }]
+ }).insert()
+
+
+def create_payment_term(name):
+ if not frappe.db.exists('Payment Term', name):
+ frappe.get_doc({
+ 'doctype': 'Payment Term',
+ 'payment_term_name': name
+ }).insert()
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index b6a664393e8..8f5e9fbc286 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -1,343 +1,107 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2016-06-01 16:55:32.196722",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "reference_doctype",
+ "reference_name",
+ "due_date",
+ "bill_no",
+ "payment_term",
+ "column_break_4",
+ "total_amount",
+ "outstanding_amount",
+ "allocated_amount",
+ "exchange_rate"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 2,
- "fetch_if_empty": 0,
"fieldname": "reference_doctype",
"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": "Type",
- "length": 0,
- "no_copy": 0,
"options": "DocType",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 2,
- "fetch_if_empty": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Name",
- "length": 0,
- "no_copy": 0,
"options": "reference_doctype",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "due_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Due Date",
- "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
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "bill_no",
"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": "Supplier Invoice No",
- "length": 0,
"no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 2,
- "fetch_if_empty": 0,
"fieldname": "total_amount",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Total Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 2,
- "fetch_if_empty": 0,
"fieldname": "outstanding_amount",
"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": "Outstanding",
- "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
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 2,
- "fetch_if_empty": 0,
"fieldname": "allocated_amount",
"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": "Allocated",
- "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
+ "label": "Allocated"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:(doc.reference_doctype=='Purchase Invoice')",
- "fetch_if_empty": 0,
"fieldname": "exchange_rate",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Exchange Rate",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
+ },
+ {
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "label": "Payment Term",
+ "options": "Payment Term"
}
],
- "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": "2019-05-01 13:24:56.586677",
+ "links": [],
+ "modified": "2020-03-13 12:07:19.362539",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
- "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
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index f2b0d3db7a1..4e4eac0610f 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -326,7 +326,7 @@ def make_payment_request(**args):
"reference_doctype": args.dt,
"reference_name": args.dn,
"party_type": args.get("party_type") or "Customer",
- "party": args.get("party") or ref_doc.customer,
+ "party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account
})
@@ -420,7 +420,7 @@ def make_payment_entry(docname):
def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
-
+
for ref in doc.references:
payment_request_name = frappe.db.get_value("Payment Request",
{"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name,
@@ -430,7 +430,7 @@ def update_payment_req_status(doc, method):
ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency)
pay_req_doc = frappe.get_doc('Payment Request', payment_request_name)
status = pay_req_doc.status
-
+
if status != "Paid" and not ref_details.outstanding_amount:
status = 'Paid'
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
index 1b38904f6da..d363cf161b5 100644
--- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
+++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
@@ -1,243 +1,82 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-08-10 15:38:00.080575",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-08-10 15:38:00.080575",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_term",
+ "description",
+ "due_date",
+ "invoice_portion",
+ "payment_amount",
+ "mode_of_payment",
+ "paid_amount"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "payment_term",
- "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": "Payment Term",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payment Term",
+ "options": "Payment Term",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fetch_from": "",
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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
- },
+ "columns": 2,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "due_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Due Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Due Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fetch_from": "",
- "fieldname": "invoice_portion",
- "fieldtype": "Percent",
- "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": "Invoice Portion",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Percent",
+ "in_list_view": 1,
+ "label": "Invoice Portion",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "payment_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Amount",
- "length": 0,
- "no_copy": 0,
- "options": "currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "payment_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Payment Amount",
+ "options": "currency",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "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": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "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": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "paid_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Amount"
}
- ],
- "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-09-06 17:35:44.580209",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Schedule",
- "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
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-03-13 17:58:24.729526",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Schedule",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json
index 7a3483d6c32..c4a2a888182 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json
@@ -1,164 +1,84 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:template_name",
- "beta": 0,
- "creation": "2017-08-10 15:34:28.058054",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:template_name",
+ "creation": "2017-08-10 15:34:28.058054",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "template_name",
+ "allocate_payment_based_on_payment_terms",
+ "terms"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "template_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": "Template Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "template_name",
+ "fieldtype": "Data",
+ "label": "Template Name",
+ "unique": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "terms",
- "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": "Payment Terms",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Terms Template Detail",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "terms",
+ "fieldtype": "Table",
+ "label": "Payment Terms",
+ "options": "Payment Terms Template Detail",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "description": "If this checkbox is checked, paid amount will be splitted and allocated as per the amounts in payment schedule against each payment term",
+ "fieldname": "allocate_payment_based_on_payment_terms",
+ "fieldtype": "Check",
+ "label": "Allocate Payment Based On Payment Terms"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-01-24 11:13:31.158613",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Terms Template",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2020-04-01 15:35:18.112619",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Terms Template",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index eb95e458dc8..b59a177f43b 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -7,6 +7,8 @@ from frappe.utils import flt
from frappe import _
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions,
+ get_dimension_filters)
class PeriodClosingVoucher(AccountsController):
def validate(self):
@@ -49,7 +51,15 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
gl_entries = []
net_pl_balance = 0
- pl_accounts = self.get_pl_balances()
+ dimension_fields = ['t1.cost_center']
+
+ accounting_dimensions = get_accounting_dimensions()
+ for dimension in accounting_dimensions:
+ dimension_fields.append('t1.{0}'.format(dimension))
+
+ dimension_filters, default_dimensions = get_dimension_filters()
+
+ pl_accounts = self.get_pl_balances(dimension_fields)
for acc in pl_accounts:
if flt(acc.balance_in_company_currency):
@@ -65,34 +75,41 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.balance_in_account_currency) > 0 else 0,
"credit": abs(flt(acc.balance_in_company_currency)) \
if flt(acc.balance_in_company_currency) > 0 else 0
- }))
+ }, item=acc))
net_pl_balance += flt(acc.balance_in_company_currency)
if net_pl_balance:
cost_center = frappe.db.get_value("Company", self.company, "cost_center")
- gl_entries.append(self.get_gl_dict({
+ gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"cost_center": cost_center
- }))
+ })
+
+ for dimension in accounting_dimensions:
+ gl_entry.update({
+ dimension: default_dimensions.get(self.company, {}).get(dimension)
+ })
+
+ gl_entries.append(gl_entry)
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
- def get_pl_balances(self):
+ def get_pl_balances(self, dimension_fields):
"""Get balance for pl accounts"""
return frappe.db.sql("""
select
- t1.account, t1.cost_center, t2.account_currency,
+ t1.account, t2.account_currency, {dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency,
sum(t1.debit) - sum(t1.credit) as balance_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s
and t1.posting_date between %s and %s
- group by t1.account, t1.cost_center
- """, (self.company, self.get("year_start_date"), self.posting_date), as_dict=1)
+ group by t1.account, {dimension_fields}
+ """.format(dimension_fields = ', '.join(dimension_fields)), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1)
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index fc0d8b16f71..086f42b293f 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -241,6 +241,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
item_details.update({
'apply_rule_on_other_items': json.dumps(pricing_rule.apply_rule_on_other_items),
+ 'price_or_product_discount': pricing_rule.price_or_product_discount,
'apply_rule_on': (frappe.scrub(pricing_rule.apply_rule_on_other)
if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on')))
})
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 100bb1d3e38..b358f56671f 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -330,9 +330,9 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
if pr_doc.mixed_conditions:
amt = args.get('qty') * args.get("price_list_rate")
if args.get("item_code") != row.get("item_code"):
- amt = row.get('qty') * (row.get("price_list_rate") or args.get("rate"))
+ amt = flt(row.get('qty')) * flt(row.get("price_list_rate") or args.get("rate"))
- sum_qty += row.get("stock_qty") or args.get("stock_qty") or args.get("qty")
+ sum_qty += flt(row.get("stock_qty")) or flt(args.get("stock_qty")) or flt(args.get("qty"))
sum_amt += amt
if pr_doc.is_cumulative:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index c5c54837a7b..3cf4d5994a5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -174,7 +174,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
read_only: 0,
fieldtype:'Date',
label: __('Release Date'),
- default: me.frm.doc.release_date
+ default: me.frm.doc.release_date,
+ reqd: 1
},
{
fieldname: 'hold_comment',
@@ -260,12 +261,25 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
price_list: this.frm.doc.buying_price_list
}, function() {
me.apply_pricing_rule();
-
me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
+ me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
+ me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
})
},
+ apply_tds: function(frm) {
+ var me = this;
+
+ if (!me.frm.doc.apply_tds) {
+ me.frm.set_value("tax_withholding_category", '');
+ me.frm.set_df_property("tax_withholding_category", "hidden", 1);
+ } else {
+ me.frm.set_value("tax_withholding_category", me.frm.supplier_tds);
+ me.frm.set_df_property("tax_withholding_category", "hidden", 0);
+ }
+ },
+
credit_to: function() {
var me = this;
if(this.frm.doc.credit_to) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 7725994c6b0..a1a20de0503 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -16,6 +16,7 @@
"is_paid",
"is_return",
"apply_tds",
+ "tax_withholding_category",
"column_break1",
"company",
"posting_date",
@@ -73,9 +74,9 @@
"base_total",
"base_net_total",
"column_break_28",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_49",
@@ -1284,13 +1285,21 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category",
+ "print_hide": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-30 19:13:49.610538",
+ "modified": "2020-04-18 13:05:25.199832",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 3a0c1239927..3654f6ef032 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -959,7 +959,7 @@ class PurchaseInvoice(BuyingController):
if not self.apply_tds:
return
- tax_withholding_details = get_party_tax_withholding_details(self)
+ tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
if not tax_withholding_details:
return
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js
index 7941a59ac36..1ed4b92e7a4 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/india.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js
@@ -25,18 +25,26 @@ frappe.ui.form.on("Sales Invoice", {
if(frm.doc.docstatus == 1 && !frm.is_dirty()
&& !frm.doc.is_return && !frm.doc.ewaybill) {
- frm.add_custom_button('e-Way Bill JSON', () => {
- var w = window.open(
- frappe.urllib.get_full_url(
- "/api/method/erpnext.regional.india.utils.generate_ewb_json?"
- + "dt=" + encodeURIComponent(frm.doc.doctype)
- + "&dn=" + encodeURIComponent(frm.doc.name)
- )
- );
- if (!w) {
- frappe.msgprint(__("Please enable pop-ups")); return;
- }
- }, __("Make"));
+ frm.add_custom_button('E-Way Bill JSON', () => {
+ frappe.call({
+ method: 'erpnext.regional.india.utils.generate_ewb_json',
+ args: {
+ 'dt': frm.doc.doctype,
+ 'dn': [frm.doc.name]
+ },
+ callback: function(r) {
+ if (r.message) {
+ const args = {
+ cmd: 'erpnext.regional.india.utils.download_ewb_json',
+ data: r.message,
+ docname: frm.doc.name
+ };
+ open_url_post(frappe.request.url, args);
+ }
+ }
+ });
+
+ }, __("Create"));
}
},
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
index 66d74b4b06a..05c4f1f1dd3 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
@@ -16,17 +16,23 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
}
}
- var w = window.open(
- frappe.urllib.get_full_url(
- "/api/method/erpnext.regional.india.utils.generate_ewb_json?"
- + "dt=" + encodeURIComponent(doclist.doctype)
- + "&dn=" + encodeURIComponent(docnames)
- )
- );
- if (!w) {
- frappe.msgprint(__("Please enable pop-ups")); return;
- }
-
+ frappe.call({
+ method: 'erpnext.regional.india.utils.generate_ewb_json',
+ args: {
+ 'dt': doclist.doctype,
+ 'dn': docnames
+ },
+ callback: function(r) {
+ if (r.message) {
+ const args = {
+ cmd: 'erpnext.regional.india.utils.download_ewb_json',
+ data: r.message,
+ docname: docnames
+ };
+ open_url_post(frappe.request.url, args);
+ }
+ }
+ });
};
doclist.page.add_actions_menu_item(__('Generate e-Way Bill JSON'), action, false);
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 7a7d25aadcc..18a791d38ce 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -32,6 +32,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
me.frm.script_manager.trigger("is_pos");
me.frm.refresh_fields();
}
+ erpnext.queries.setup_warehouse_query(this.frm);
},
refresh: function(doc, dt, dn) {
@@ -586,7 +587,9 @@ frappe.ui.form.on('Sales Invoice', {
frm.set_query("account_for_change_amount", function() {
return {
filters: {
- account_type: ['in', ["Cash", "Bank"]]
+ account_type: ['in', ["Cash", "Bank"]],
+ company: frm.doc.company,
+ is_group: 0
}
};
});
@@ -667,7 +670,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.fields_dict["loyalty_redemption_account"].get_query = function() {
return {
filters:{
- "company": frm.doc.company
+ "company": frm.doc.company,
+ "is_group": 0
}
}
};
@@ -676,7 +680,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.fields_dict["loyalty_redemption_cost_center"].get_query = function() {
return {
filters:{
- "company": frm.doc.company
+ "company": frm.doc.company,
+ "is_group": 0
}
}
};
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 52a0f4e081d..ea78c27333e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:05",
@@ -74,9 +75,9 @@
"base_total",
"base_net_total",
"column_break_32",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"taxes_and_charges",
"column_break_38",
@@ -148,9 +149,9 @@
"edit_printing_settings",
"letter_head",
"group_same_items",
- "language",
- "column_break_84",
"select_print_heading",
+ "column_break_84",
+ "language",
"more_information",
"inter_company_invoice_reference",
"customer_group",
@@ -1568,7 +1569,8 @@
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
- "modified": "2020-02-10 04:57:11.221180",
+ "links": [],
+ "modified": "2020-04-29 13:37:09.355300",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index b8c45aba1f5..5218b740edd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -432,11 +432,12 @@ class SalesInvoice(SellingController):
if pos.get("company_address"):
self.company_address = pos.get("company_address")
- customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
-
- customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list')
-
- selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
+ if self.customer:
+ customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
+ customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list')
+ selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
+ else:
+ selling_price_list = pos.get('selling_price_list')
if selling_price_list:
self.set('selling_price_list', selling_price_list)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ad0e9572497..0b06342f309 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1834,7 +1834,7 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
- data = get_ewb_data("Sales Invoice", si.name)
+ data = get_ewb_data("Sales Invoice", [si.name])
self.assertEqual(data['version'], '1.0.1118')
self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR')
@@ -1868,16 +1868,6 @@ class TestSalesInvoice(unittest.TestCase):
item.taxes = []
item.save()
- def test_customer_provided_parts_si(self):
- create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- si = create_sales_invoice(item_code='CUST-0987', rate=0)
- self.assertEqual(si.get("items")[0].allow_zero_valuation_rate, 1)
- self.assertEqual(si.get("items")[0].amount, 0)
-
- # test if Sales Invoice with rate is allowed
- si2 = create_sales_invoice(item_code='CUST-0987', do_not_save=True)
- self.assertRaises(frappe.ValidationError, si2.save)
-
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 6c31e9efed1..dd6b4fdc603 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -6,23 +6,42 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import flt
+from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
pass
-def get_party_tax_withholding_details(ref_doc):
- tax_withholding_category = frappe.db.get_value('Supplier', ref_doc.supplier, 'tax_withholding_category')
+def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None):
+
+ pan_no = ''
+ suppliers = []
+
+ if not tax_withholding_category:
+ tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan'])
+
if not tax_withholding_category:
return
+ if not pan_no:
+ pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan')
+
+ # Get others suppliers with the same PAN No
+ if pan_no:
+ suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})]
+
+ if not suppliers:
+ suppliers.append(ref_doc.supplier)
+
fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company)
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
.format(tax_withholding_category, ref_doc.company))
- tds_amount = get_tds_amount(ref_doc, tax_details, fy)
+
+ tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company,
+ tax_details, fy, ref_doc.posting_date, pan_no)
+
tax_row = get_tax_row(tax_details, tds_amount)
return tax_row
@@ -51,6 +70,7 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year):
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
def get_tax_row(tax_details, tds_amount):
+
return {
"category": "Total",
"add_deduct_tax": "Deduct",
@@ -60,25 +80,36 @@ def get_tax_row(tax_details, tds_amount):
"tax_amount": tds_amount
}
-def get_tds_amount(ref_doc, tax_details, fiscal_year_details):
+def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None):
fiscal_year, year_start_date, year_end_date = fiscal_year_details
tds_amount = 0
tds_deducted = 0
- def _get_tds(amount):
+ def _get_tds(amount, rate):
if amount <= 0:
return 0
- return amount * tax_details.rate / 100
+ return amount * rate / 100
+
+ ldc_name = frappe.db.get_value('Lower Deduction Certificate',
+ {
+ 'pan_no': pan_no,
+ 'fiscal_year': fiscal_year
+ }, 'name')
+ ldc = ''
+
+ if ldc_name:
+ ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name)
entries = frappe.db.sql("""
select voucher_no, credit
from `tabGL Entry`
- where party=%s and fiscal_year=%s and credit > 0
- """, (ref_doc.supplier, fiscal_year), as_dict=1)
+ where company = %s and
+ party in %s and fiscal_year=%s and credit > 0
+ """, (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries]
- advance_vouchers = get_advance_vouchers(ref_doc.supplier, fiscal_year)
+ advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company)
tds_vouchers = vouchers + advance_vouchers
@@ -93,7 +124,20 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details):
tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0
if tds_deducted:
- tds_amount = _get_tds(ref_doc.net_total)
+ if ldc:
+ limit_consumed = frappe.db.get_value('Purchase Invoice',
+ {
+ 'supplier': ('in', suppliers),
+ 'apply_tds': 1,
+ 'docstatus': 1
+ }, 'sum(net_total)')
+
+ if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total,
+ ldc.certificate_limit):
+
+ tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
+ else:
+ tds_amount = _get_tds(net_total, tax_details.rate)
else:
supplier_credit_amount = frappe.get_all('Purchase Invoice Item',
fields = ['sum(net_amount)'],
@@ -106,43 +150,79 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details):
fields = ['sum(credit_in_account_currency)'],
filters = {
'parent': ('in', vouchers), 'docstatus': 1,
- 'party': ref_doc.supplier,
+ 'party': ('in', suppliers),
'reference_type': ('not in', ['Purchase Invoice'])
}, as_list=1)
supplier_credit_amount += (jv_supplier_credit_amt[0][0]
if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0)
- supplier_credit_amount += ref_doc.net_total
+ supplier_credit_amount += net_total
- debit_note_amount = get_debit_note_amount(ref_doc.supplier, year_start_date, year_end_date)
+ debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
supplier_credit_amount -= debit_note_amount
if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold)
or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)):
- tds_amount = _get_tds(supplier_credit_amount)
+
+ if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
+ ldc.certificate_limit):
+ tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate,
+ tax_details)
+ else:
+ tds_amount = _get_tds(supplier_credit_amount, tax_details.rate)
return tds_amount
-def get_advance_vouchers(supplier, fiscal_year=None, company=None, from_date=None, to_date=None):
+def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None):
condition = "fiscal_year=%s" % fiscal_year
+
+ if company:
+ condition += "and company =%s" % (company)
if from_date and to_date:
- condition = "company=%s and posting_date between %s and %s" % (company, from_date, to_date)
+ condition += "and posting_date between %s and %s" % (company, from_date, to_date)
+
+ ## Appending the same supplier again if length of suppliers list is 1
+ ## since tuple of single element list contains None, For example ('Test Supplier 1', )
+ ## and the below query fails
+ if len(suppliers) == 1:
+ suppliers.append(suppliers[0])
return frappe.db.sql_list("""
select distinct voucher_no
from `tabGL Entry`
- where party=%s and %s and debit > 0
- """, (supplier, condition)) or []
+ where party in %s and %s and debit > 0
+ """, (tuple(suppliers), condition)) or []
-def get_debit_note_amount(supplier, year_start_date, year_end_date, company=None):
- condition = ""
+def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
+ condition = "and 1=1"
if company:
condition = " and company=%s " % company
+ if len(suppliers) == 1:
+ suppliers.append(suppliers[0])
+
return flt(frappe.db.sql("""
select abs(sum(net_total))
from `tabPurchase Invoice`
- where supplier=%s %s and is_return=1 and docstatus=1
- and posting_date between %s and %s
- """, (supplier, condition, year_start_date, year_end_date)))
\ No newline at end of file
+ where supplier in %s and is_return=1 and docstatus=1
+ and posting_date between %s and %s %s
+ """, (tuple(suppliers), year_start_date, year_end_date, condition)))
+
+def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
+ if current_amount < (certificate_limit - deducted_amount):
+ return current_amount * rate/100
+ else:
+ ltds_amount = (certificate_limit - deducted_amount)
+ tds_amount = current_amount - ltds_amount
+
+ return ltds_amount * rate/100 + tds_amount * tax_details.rate/100
+
+def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit):
+ valid = False
+
+ if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and
+ certificate_limit > deducted_amount):
+ valid = True
+
+ return valid
\ No newline at end of file
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 3d9ff273ce2..b5dfbde5a04 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -163,7 +163,7 @@ def get_default_price_list(party):
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list
price_list = get_permitted_documents('Price List')
-
+
# if there is only one permitted document based on user permissions, set it
if price_list and len(price_list) == 1:
price_list = price_list[0]
@@ -468,23 +468,25 @@ def get_timeline_data(doctype, name):
from frappe.desk.form.load import get_communication_data
out = {}
- fields = 'date(creation), count(name)'
+ fields = 'creation, count(*)'
after = add_years(None, -1).strftime('%Y-%m-%d')
- group_by='group by date(creation)'
+ group_by='group by Date(creation)'
- data = get_communication_data(doctype, name, after=after, group_by='group by date(creation)',
- fields='date(C.creation) as creation, count(C.name)',as_dict=False)
+ data = get_communication_data(doctype, name, after=after, group_by='group by creation',
+ fields='C.creation as creation, count(C.name)',as_dict=False)
# fetch and append data from Activity Log
data += frappe.db.sql("""select {fields}
from `tabActivity Log`
- where (reference_doctype="{doctype}" and reference_name="{name}")
- or (timeline_doctype in ("{doctype}") and timeline_name="{name}")
- or (reference_doctype in ("Quotation", "Opportunity") and timeline_name="{name}")
+ where (reference_doctype=%(doctype)s and reference_name=%(name)s)
+ or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s)
+ or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s)
and status!='Success' and creation > {after}
{group_by} order by creation desc
- """.format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields,
- group_by=group_by, after=after), as_dict=False)
+ """.format(fields=fields, group_by=group_by, after=after), {
+ "doctype": doctype,
+ "name": name
+ }, as_dict=False)
timeline_items = dict(data)
@@ -603,10 +605,12 @@ def get_party_shipping_address(doctype, name):
else:
return ''
-def get_partywise_advanced_payment_amount(party_type, posting_date = None):
+def get_partywise_advanced_payment_amount(party_type, posting_date = None, company=None):
cond = "1=1"
if posting_date:
cond = "posting_date <= '{0}'".format(posting_date)
+ if company:
+ cond += "and company = '{0}'".format(company)
data = frappe.db.sql(""" SELECT party, sum({0}) as amount
FROM `tabGL Entry`
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 240b0d83817..e9c286fcf0d 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -344,26 +344,28 @@ class ReceivablePayableReport(object):
def allocate_outstanding_based_on_payment_terms(self, row):
self.get_payment_terms(row)
for term in row.payment_terms:
- term.outstanding = term.invoiced
# update "paid" and "oustanding" for this term
- self.allocate_closing_to_term(row, term, 'paid')
+ if not term.paid:
+ self.allocate_closing_to_term(row, term, 'paid')
# update "credit_note" and "oustanding" for this term
if term.outstanding:
self.allocate_closing_to_term(row, term, 'credit_note')
+ row.payment_terms = sorted(row.payment_terms, key=lambda x: x['due_date'])
+
def get_payment_terms(self, row):
# build payment_terms for row
payment_terms_details = frappe.db.sql("""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
- ps.due_date, ps.payment_amount, ps.description
+ ps.due_date, ps.payment_amount, ps.description, ps.paid_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
si.name = %s
- order by ps.due_date
+ order by ps.paid_amount desc, due_date
""".format(row.voucher_type), row.voucher_no, as_dict = 1)
@@ -389,11 +391,14 @@ class ReceivablePayableReport(object):
"invoiced": invoiced,
"invoice_grand_total": row.invoiced,
"payment_term": d.description,
- "paid": 0.0,
+ "paid": d.paid_amount,
"credit_note": 0.0,
- "outstanding": 0.0
+ "outstanding": invoiced - d.paid_amount
}))
+ if d.paid_amount:
+ row['paid'] -= d.paid_amount
+
def allocate_closing_to_term(self, row, term, key):
if row[key]:
if row[key] > term.outstanding:
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index b607c0f7028..aa6b42e89d0 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -33,7 +33,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.get_party_total(args)
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
- self.filters.report_date) or {}
+ self.filters.report_date, self.filters.company) or {}
for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding == 0:
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index 9a2205a5791..378fa3791c1 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -2,7 +2,7 @@
{% if (filters.party_name) { %}
{%= filters.party_name %}
- {% } else if (filters.party) { %}
+ {% } else if (filters.party && filters.party.length) { %}
{%= filters.party %}
{% } else if (filters.account) { %}
{%= filters.account %}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 6f74e8734e3..ae407231caa 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -293,6 +293,9 @@ def get_accountwise_gle(filters, gl_entries, gle_map):
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
+ if data[key].against_voucher:
+ data[key].against_voucher += ', ' + gle.against_voucher
+
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
for gle in gl_entries:
if (gle.posting_date < from_date or
@@ -365,6 +368,7 @@ def get_columns(filters):
columns = [
{
+ "label": _("GL Entry"),
"fieldname": "gl_entry",
"fieldtype": "Link",
"options": "GL Entry",
diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
index 6550981a146..8ce745e4d6b 100644
--- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
+++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
@@ -8,7 +8,6 @@ from frappe.utils import flt
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data)
import copy
-
def execute(filters=None):
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
filters.periodicity, filters.accumulated_values, filters.company)
@@ -27,17 +26,26 @@ def execute(filters=None):
gross_income = get_revenue(income, period_list)
-
gross_expense = get_revenue(expense, period_list)
if(len(gross_income)==0 and len(gross_expense)== 0):
- data.append({"account_name": "'" + _("Nothing is included in gross") + "'",
- "account": "'" + _("Nothing is included in gross") + "'"})
-
+ data.append({
+ "account_name": "'" + _("Nothing is included in gross") + "'",
+ "account": "'" + _("Nothing is included in gross") + "'"
+ })
return columns, data
- data.append({"account_name": "'" + _("Included in Gross Profit") + "'",
- "account": "'" + _("Included in Gross Profit") + "'"})
+
+ # to avoid error eg: gross_income[0] : list index out of range
+ if not gross_income:
+ gross_income = [{}]
+ if not gross_expense:
+ gross_expense = [{}]
+
+ data.append({
+ "account_name": "'" + _("Included in Gross Profit") + "'",
+ "account": "'" + _("Included in Gross Profit") + "'"
+ })
data.append({})
data.extend(gross_income or [])
@@ -111,7 +119,6 @@ def set_total(node, value, complete_list, totals):
def get_profit(gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False):
-
profit_loss = {
"account_name": "'" + _(profit_type) + "'",
"account": "'" + _(profit_type) + "'",
@@ -123,7 +130,9 @@ def get_profit(gross_income, gross_expense, period_list, company, profit_type, c
for period in period_list:
key = period if consolidated else period.key
- profit_loss[key] = flt(gross_income[0].get(key, 0)) - flt(gross_expense[0].get(key, 0))
+ gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
+ gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
+ profit_loss[key] = gross_income_for_period - gross_expense_for_period
if profit_loss[key]:
has_value=True
@@ -143,12 +152,18 @@ def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expe
for period in period_list:
key = period if consolidated else period.key
- total_income = flt(gross_income[0].get(key, 0)) + flt(non_gross_income[0].get(key, 0))
- total_expense = flt(gross_expense[0].get(key, 0)) + flt(non_gross_expense[0].get(key, 0))
+ gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
+ non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0
+
+ gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
+ non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0
+
+ total_income = gross_income_for_period + non_gross_income_for_period
+ total_expense = gross_expense_for_period + non_gross_expense_for_period
profit_loss[key] = flt(total_income) - flt(total_expense)
if profit_loss[key]:
has_value=True
if has_value:
- return profit_loss
+ return profit_loss
\ No newline at end of file
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 6ef6d6eea03..4e22b05a81d 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -55,27 +55,27 @@ def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
- "posting_date": _("Posting Date") + ":Date",
- "posting_time": _("Posting Time"),
- "item_code": _("Item Code") + ":Link/Item",
- "item_name": _("Item Name"),
- "item_group": _("Item Group") + ":Link/Item Group",
- "brand": _("Brand"),
- "description": _("Description"),
- "warehouse": _("Warehouse") + ":Link/Warehouse",
- "qty": _("Qty") + ":Float",
- "base_rate": _("Avg. Selling Rate") + ":Currency/currency",
- "buying_rate": _("Valuation Rate") + ":Currency/currency",
- "base_amount": _("Selling Amount") + ":Currency/currency",
- "buying_amount": _("Buying Amount") + ":Currency/currency",
- "gross_profit": _("Gross Profit") + ":Currency/currency",
- "gross_profit_percent": _("Gross Profit %") + ":Percent",
- "project": _("Project") + ":Link/Project",
+ "posting_date": _("Posting Date") + ":Date:100",
+ "posting_time": _("Posting Time") + ":Data:100",
+ "item_code": _("Item Code") + ":Link/Item:100",
+ "item_name": _("Item Name") + ":Data:100",
+ "item_group": _("Item Group") + ":Link/Item Group:100",
+ "brand": _("Brand") + ":Link/Brand:100",
+ "description": _("Description") +":Data:100",
+ "warehouse": _("Warehouse") + ":Link/Warehouse:100",
+ "qty": _("Qty") + ":Float:80",
+ "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100",
+ "buying_rate": _("Valuation Rate") + ":Currency/currency:100",
+ "base_amount": _("Selling Amount") + ":Currency/currency:100",
+ "buying_amount": _("Buying Amount") + ":Currency/currency:100",
+ "gross_profit": _("Gross Profit") + ":Currency/currency:100",
+ "gross_profit_percent": _("Gross Profit %") + ":Percent:100",
+ "project": _("Project") + ":Link/Project:100",
"sales_person": _("Sales person"),
- "allocated_amount": _("Allocated Amount") + ":Currency/currency",
- "customer": _("Customer") + ":Link/Customer",
- "customer_group": _("Customer Group") + ":Link/Customer Group",
- "territory": _("Territory") + ":Link/Territory"
+ "allocated_amount": _("Allocated Amount") + ":Currency/currency:100",
+ "customer": _("Customer") + ":Link/Customer:100",
+ "customer_group": _("Customer Group") + ":Link/Customer Group:100",
+ "territory": _("Territory") + ":Link/Territory:100"
})
for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -85,7 +85,8 @@ def get_columns(group_wise_columns, filters):
"fieldname": "currency",
"label" : _("Currency"),
"fieldtype": "Link",
- "options": "Currency"
+ "options": "Currency",
+ "hidden": 1
})
return columns
@@ -277,7 +278,7 @@ class GrossProfitGenerator(object):
from `tabPurchase Invoice Item` a
where a.item_code = %s and a.docstatus=1
and modified <= %s
- order by a.modified desc limit 1""", (item_code,self.filters.to_date))
+ order by a.modified desc limit 1""", (item_code, self.filters.to_date))
else:
last_purchase_rate = frappe.db.sql("""
select (a.base_rate / a.conversion_factor)
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index 127f3133f5b..1f78c7a006f 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -102,7 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
data.append(row)
- if filters.get('group_by'):
+ if filters.get('group_by') and item_list:
total_row = total_row_map.get(prev_group_by_value or d.get('item_name'))
total_row['percent_gt'] = flt(total_row['total']/grand_total * 100)
data.append(total_row)
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 0c8957ae441..92a22e62f14 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -111,7 +111,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
data.append(row)
- if filters.get('group_by'):
+ if filters.get('group_by') and item_list:
total_row = total_row_map.get(prev_group_by_value or d.get('item_name'))
total_row['percent_gt'] = flt(total_row['total']/grand_total * 100)
data.append(total_row)
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 2e805f8d3f5..c7cfee74cb0 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -44,9 +44,14 @@ def get_result(filters):
out = []
for supplier in filters.supplier:
tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category)
- rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year][0]
+ rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year]
+
+ if rate:
+ rate = rate[0]
+
try:
account = [d.account for d in tds.accounts if d.company == filters.company][0]
+
except IndexError:
account = []
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
@@ -76,7 +81,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date):
supplier_credit_amount = flt(sum([d.credit for d in entries]))
vouchers = [d.voucher_no for d in entries]
- vouchers += get_advance_vouchers(supplier, company=company,
+ vouchers += get_advance_vouchers([supplier], company=company,
from_date=from_date, to_date=to_date)
tds_deducted = 0
@@ -89,7 +94,7 @@ 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)
+ debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, 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 5ad8cb50d5f..d1aa4011b63 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -817,48 +817,37 @@ def create_payment_gateway_account(gateway):
pass
@frappe.whitelist()
-def update_number_field(doctype_name, name, field_name, number_value, company):
+def update_cost_center(docname, cost_center_name, cost_center_number, company):
'''
- doctype_name = Name of the DocType
- name = Docname being referred
- field_name = Name of the field thats holding the 'number' attribute
- number_value = Numeric value entered in field_name
-
- Stores the number entered in the dialog to the DocType's field.
-
Renames the document by adding the number as a prefix to the current name and updates
all transaction where it was present.
'''
- doc_title = frappe.db.get_value(doctype_name, name, frappe.scrub(doctype_name)+"_name")
+ validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number")
- validate_field_number(doctype_name, name, number_value, company, field_name)
+ if cost_center_number:
+ frappe.db.set_value("Cost Center", docname, "cost_center_number", cost_center_number.strip())
+ else:
+ frappe.db.set_value("Cost Center", docname, "cost_center_number", "")
- frappe.db.set_value(doctype_name, name, field_name, number_value)
+ frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip())
- if doc_title[0].isdigit():
- separator = " - " if " - " in doc_title else " "
- doc_title = doc_title.split(separator, 1)[1]
-
- frappe.db.set_value(doctype_name, name, frappe.scrub(doctype_name)+"_name", doc_title)
-
- new_name = get_autoname_with_number(number_value, doc_title, name, company)
-
- if name != new_name:
- frappe.rename_doc(doctype_name, name, new_name)
+ new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company)
+ if docname != new_name:
+ frappe.rename_doc("Cost Center", docname, new_name, force=1)
return new_name
-def validate_field_number(doctype_name, name, number_value, company, field_name):
+def validate_field_number(doctype_name, docname, number_value, company, field_name):
''' Validate if the number entered isn't already assigned to some other document. '''
if number_value:
+ filters = {field_name: number_value, "name": ["!=", docname]}
if company:
- doctype_with_same_number = frappe.db.get_value(doctype_name,
- {field_name: number_value, "company": company, "name": ["!=", name]})
- else:
- doctype_with_same_number = frappe.db.get_value(doctype_name,
- {field_name: number_value, "name": ["!=", name]})
+ filters["company"] = company
+
+ doctype_with_same_number = frappe.db.get_value(doctype_name, filters)
+
if doctype_with_same_number:
- frappe.throw(_("{0} Number {1} already used in account {2}")
- .format(doctype_name, number_value, doctype_with_same_number))
+ frappe.throw(_("{0} Number {1} is already used in {2} {3}")
+ .format(doctype_name, number_value, doctype_name.lower(), doctype_with_same_number))
def get_autoname_with_number(number_value, doc_title, name, company):
''' append title with prefix as number and suffix as company's abbreviation separated by '-' '''
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 0df6a6c7de0..b8daf1fead9 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -22,6 +22,7 @@ class Asset(AccountsController):
self.validate_item()
self.set_missing_values()
self.prepare_depreciation_data()
+ self.validate_gross_and_purchase_amount()
if self.get("schedules"):
self.validate_expected_value_after_useful_life()
@@ -31,7 +32,7 @@ class Asset(AccountsController):
self.validate_in_use_date()
self.set_status()
self.make_asset_movement()
- if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category):
+ if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
def before_cancel(self):
@@ -123,6 +124,12 @@ class Asset(AccountsController):
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
frappe.throw(_("Available-for-use Date should be after purchase date"))
+
+ def validate_gross_and_purchase_amount(self):
+ if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
+ frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\
+ Please do not book expense of multiple assets against one single Asset.")
+ .format(frappe.bold("equal"), "
"), title=_("Invalid Gross Purchase Amount"))
def cancel_auto_gen_movement(self):
movements = frappe.db.sql(
@@ -447,18 +454,55 @@ class Asset(AccountsController):
for d in self.get('finance_books'):
if d.finance_book == self.default_finance_book:
return cint(d.idx) - 1
+
+ def validate_make_gl_entry(self):
+ purchase_document = self.get_purchase_document()
+ asset_bought_with_invoice = purchase_document == self.purchase_invoice
+ fixed_asset_account, cwip_account = self.get_asset_accounts()
+ cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
+ # check if expense already has been booked in case of cwip was enabled after purchasing asset
+ expense_booked = False
+ cwip_booked = False
+
+ if asset_bought_with_invoice:
+ expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
+ (purchase_document, fixed_asset_account), as_dict=1)
+ else:
+ cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
+ (purchase_document, cwip_account), as_dict=1)
+
+ if cwip_enabled and (expense_booked or not cwip_booked):
+ # if expense has already booked from invoice or cwip is booked from receipt
+ return False
+ elif not cwip_enabled and (not expense_booked or cwip_booked):
+ # if cwip is disabled but expense hasn't been booked yet
+ return True
+ elif cwip_enabled:
+ # default condition
+ return True
+
+ def get_purchase_document(self):
+ asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
+ purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt
+
+ return purchase_document
+
+ def get_asset_accounts(self):
+ fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
+ asset_category = self.asset_category, company = self.company)
+
+ cwip_account = get_asset_account("capital_work_in_progress_account",
+ self.name, self.asset_category, self.company)
+
+ return fixed_asset_account, cwip_account
def make_gl_entries(self):
gl_entries = []
- if ((self.purchase_receipt \
- or (self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')))
- and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
- fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
- asset_category = self.asset_category, company = self.company)
+ purchase_document = self.get_purchase_document()
+ fixed_asset_account, cwip_account = self.get_asset_accounts()
- cwip_account = get_asset_account("capital_work_in_progress_account",
- self.name, self.asset_category, self.company)
+ if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
gl_entries.append(self.get_gl_dict({
"account": cwip_account,
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index a56440de3d3..e8def53c070 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -82,7 +82,6 @@ class TestAsset(unittest.TestCase):
doc.set_missing_values()
self.assertEquals(doc.items[0].is_fixed_asset, 1)
-
def test_schedule_for_straight_line_method(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=100000.0, location="Test Location")
@@ -564,6 +563,81 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle)
+ def test_gle_with_cwip_toggling(self):
+ # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
+
+ pr = make_purchase_receipt(item_code="Macbook Pro",
+ qty=1, rate=5000, do_not_submit=True, location="Test Location")
+ pr.set('taxes', [{
+ 'category': 'Total',
+ 'add_deduct_tax': 'Add',
+ 'charge_type': 'On Net Total',
+ 'account_head': '_Test Account Service Tax - _TC',
+ 'description': '_Test Account Service Tax',
+ 'cost_center': 'Main - _TC',
+ 'rate': 5.0
+ }, {
+ 'category': 'Valuation and Total',
+ 'add_deduct_tax': 'Add',
+ 'charge_type': 'On Net Total',
+ 'account_head': '_Test Account Shipping Charges - _TC',
+ 'description': '_Test Account Shipping Charges',
+ 'cost_center': 'Main - _TC',
+ 'rate': 5.0
+ }])
+ pr.submit()
+ expected_gle = (
+ ("Asset Received But Not Billed - _TC", 0.0, 5250.0),
+ ("CWIP Account - _TC", 5250.0, 0.0)
+ )
+ pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
+ where voucher_type='Purchase Receipt' and voucher_no = %s
+ order by account""", pr.name)
+ self.assertEqual(pr_gle, expected_gle)
+
+ pi = make_invoice(pr.name)
+ pi.submit()
+ expected_gle = (
+ ("_Test Account Service Tax - _TC", 250.0, 0.0),
+ ("_Test Account Shipping Charges - _TC", 250.0, 0.0),
+ ("Asset Received But Not Billed - _TC", 5250.0, 0.0),
+ ("Creditors - _TC", 0.0, 5500.0),
+ ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
+ )
+ pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
+ where voucher_type='Purchase Invoice' and voucher_no = %s
+ order by account""", pi.name)
+ self.assertEqual(pi_gle, expected_gle)
+
+ asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
+ asset_doc = frappe.get_doc('Asset', asset)
+ month_end_date = get_last_day(nowdate())
+ asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
+ self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
+ asset_doc.append("finance_books", {
+ "expected_value_after_useful_life": 200,
+ "depreciation_method": "Straight Line",
+ "total_number_of_depreciations": 3,
+ "frequency_of_depreciation": 10,
+ "depreciation_start_date": month_end_date
+ })
+
+ # disable cwip and try submitting
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
+ asset_doc.submit()
+ # asset should have gl entries even if cwip is disabled
+ expected_gle = (
+ ("_Test Fixed Asset - _TC", 5250.0, 0.0),
+ ("CWIP Account - _TC", 0.0, 5250.0)
+ )
+ gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
+ where voucher_type='Asset' and voucher_no = %s
+ order by account""", asset_doc.name)
+ self.assertEqual(gle, expected_gle)
+
+ frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
+
def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location")
@@ -599,6 +673,7 @@ def create_asset(**args):
"purchase_date": "2015-01-01",
"calculate_depreciation": 0,
"gross_purchase_amount": 100000,
+ "purchase_receipt_amount": 100000,
"expected_value_after_useful_life": 10000,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": "2020-06-06",
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index fc08841be99..9a33fc14ac0 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -11,12 +11,54 @@ from frappe.model.document import Document
class AssetCategory(Document):
def validate(self):
self.validate_finance_books()
+ self.validate_account_types()
+ self.validate_account_currency()
def validate_finance_books(self):
for d in self.finance_books:
for field in ("Total Number of Depreciations", "Frequency of Depreciation"):
if cint(d.get(frappe.scrub(field)))<1:
frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError)
+
+ def validate_account_currency(self):
+ account_types = [
+ 'fixed_asset_account', 'accumulated_depreciation_account', 'depreciation_expense_account', 'capital_work_in_progress_account'
+ ]
+ invalid_accounts = []
+ for d in self.accounts:
+ company_currency = frappe.get_value('Company', d.get('company_name'), 'default_currency')
+ for type_of_account in account_types:
+ if d.get(type_of_account):
+ account_currency = frappe.get_value("Account", d.get(type_of_account), "account_currency")
+ if account_currency != company_currency:
+ invalid_accounts.append(frappe._dict({ 'type': type_of_account, 'idx': d.idx, 'account': d.get(type_of_account) }))
+
+ for d in invalid_accounts:
+ frappe.throw(_("Row #{}: Currency of {} - {} doesn't matches company currency.")
+ .format(d.idx, frappe.bold(frappe.unscrub(d.type)), frappe.bold(d.account)),
+ title=_("Invalid Account"))
+
+
+ def validate_account_types(self):
+ account_type_map = {
+ 'fixed_asset_account': { 'account_type': 'Fixed Asset' },
+ 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
+ 'depreciation_expense_account': { 'root_type': 'Expense' },
+ 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
+ }
+ for d in self.accounts:
+ for fieldname in account_type_map.keys():
+ if d.get(fieldname):
+ selected_account = d.get(fieldname)
+ key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
+ selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
+ expected_key_type = account_type_map[fieldname][key_to_match]
+
+ if selected_key_type != expected_key_type:
+ frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
+ .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
+ title=_("Invalid Account"))
+
@frappe.whitelist()
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index 3a08baa714e..3da355e2b99 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -110,6 +110,7 @@ class AssetMovement(Document):
ORDER BY
asm.transaction_date asc
""", (d.asset, self.company, 'Receipt'), as_dict=1)
+
if auto_gen_movement_entry and auto_gen_movement_entry[0].get('name') == self.name:
frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \
auto generated for Asset {1}').format(self.name, d.asset))
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 7b1f1354d79..fa4c76f364f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -27,15 +27,6 @@ frappe.ui.form.on("Purchase Order", {
frm.set_indicator_formatter('item_code',
function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" })
- frm.set_query("blanket_order", "items", function() {
- return {
- filters: {
- "company": frm.doc.company,
- "docstatus": 1
- }
- }
- });
-
frm.set_query("expense_account", "items", function() {
return {
query: "erpnext.controllers.queries.get_expense_account",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index d82e128735e..0ccdb25c32f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -63,9 +64,9 @@
"base_total",
"base_net_total",
"column_break_26",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_50",
@@ -171,6 +172,7 @@
},
{
"depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))",
+ "description": "Fetch items based on Default Supplier.",
"fieldname": "get_items_from_open_material_requests",
"fieldtype": "Button",
"label": "Get Items from Open Material Requests"
@@ -1053,7 +1055,8 @@
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
- "modified": "2020-01-14 18:54:39.694448",
+ "links": [],
+ "modified": "2020-04-17 13:04:28.185197",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
index 866bf0c7332..39668795cba 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
@@ -141,19 +141,18 @@ def get_conditions(filters):
conditions = ""
if filters.get("company"):
- conditions += " AND company=%s"% frappe.db.escape(filters.get('company'))
+ conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company'))
if filters.get("cost_center") or filters.get("project"):
conditions += """
- AND (cost_center=%s
- OR project=%s)
- """% (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
+ AND (child.`cost_center`=%s OR child.`project`=%s)
+ """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
if filters.get("from_date"):
- conditions += " AND transaction_date>=%s"% filters.get('from_date')
+ conditions += " AND par.transaction_date>='%s'" % filters.get('from_date')
if filters.get("to_date"):
- conditions += " AND transaction_date<=%s"% filters.get('to_date')
+ conditions += " AND par.transaction_date<='%s'" % filters.get('to_date')
return conditions
def get_data(filters):
@@ -162,7 +161,6 @@ def get_data(filters):
mr_records, procurement_record_against_mr = get_mapped_mr_details(conditions)
pr_records = get_mapped_pr_records()
pi_records = get_mapped_pi_records()
- print(pi_records)
procurement_record=[]
if procurement_record_against_mr:
@@ -198,16 +196,16 @@ def get_mapped_mr_details(conditions):
mr_records = {}
mr_details = frappe.db.sql("""
SELECT
- mr.transaction_date,
- mr.per_ordered,
- mr_item.name,
- mr_item.parent,
- mr_item.amount
- FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
+ par.transaction_date,
+ par.per_ordered,
+ child.name,
+ child.parent,
+ child.amount
+ FROM `tabMaterial Request` par, `tabMaterial Request Item` child
WHERE
- mr.per_ordered>=0
- AND mr.name=mr_item.parent
- AND mr.docstatus=1
+ par.per_ordered>=0
+ AND par.name=child.parent
+ AND par.docstatus=1
{conditions}
""".format(conditions=conditions), as_dict=1) #nosec
@@ -254,29 +252,29 @@ def get_mapped_pr_records():
def get_po_entries(conditions):
return frappe.db.sql("""
SELECT
- po_item.name,
- po_item.parent,
- po_item.cost_center,
- po_item.project,
- po_item.warehouse,
- po_item.material_request,
- po_item.material_request_item,
- po_item.description,
- po_item.stock_uom,
- po_item.qty,
- po_item.amount,
- po_item.base_amount,
- po_item.schedule_date,
- po.transaction_date,
- po.supplier,
- po.status,
- po.owner
- FROM `tabPurchase Order` po, `tabPurchase Order Item` po_item
+ child.name,
+ child.parent,
+ child.cost_center,
+ child.project,
+ child.warehouse,
+ child.material_request,
+ child.material_request_item,
+ child.description,
+ child.stock_uom,
+ child.qty,
+ child.amount,
+ child.base_amount,
+ child.schedule_date,
+ par.transaction_date,
+ par.supplier,
+ par.status,
+ par.owner
+ FROM `tabPurchase Order` par, `tabPurchase Order Item` child
WHERE
- po.docstatus = 1
- AND po.name = po_item.parent
- AND po.status not in ("Closed","Completed","Cancelled")
+ par.docstatus = 1
+ AND par.name = child.parent
+ AND par.status not in ("Closed","Completed","Cancelled")
{conditions}
GROUP BY
- po.name,po_item.item_code
+ par.name, child.item_code
""".format(conditions=conditions), as_dict=1) #nosec
\ No newline at end of file
diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py
index 4e5e9037b3a..1be05a346d0 100644
--- a/erpnext/config/hr.py
+++ b/erpnext/config/hr.py
@@ -170,6 +170,10 @@ def get_data():
"type": "doctype",
"name": "Payroll Period",
},
+ {
+ "type": "doctype",
+ "name": "Income Tax Slab",
+ },
{
"type": "doctype",
"name": "Salary Component",
@@ -209,6 +213,10 @@ def get_data():
"name": "Employee Tax Exemption Proof Submission",
"dependencies": ["Employee"]
},
+ {
+ "type": "doctype",
+ "name": "Employee Other Income",
+ },
{
"type": "doctype",
"name": "Employee Benefit Application",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c17927a35c0..e5073917b8d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -819,7 +819,7 @@ class AccountsController(TransactionBase):
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
- d.payment_amount = grand_total * flt(d.invoice_portion) / 100
+ d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount'))
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -834,7 +834,7 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
- frappe.throw(_("Row {0}: Due Date cannot be before posting date").format(d.idx))
+ frappe.throw(_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx))
elif d.due_date in dates:
li.append(_("{0} in row {1}").format(d.due_date, d.idx))
dates.append(d.due_date)
@@ -1123,36 +1123,39 @@ def get_supplier_block_status(party_name):
}
return info
-def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code):
+def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
"""
Returns a Sales Order Item child item containing the default values
"""
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname)
- item = frappe.get_doc("Item", item_code)
+ item = frappe.get_doc("Item", trans_item.get('item_code'))
child_item.item_code = item.item_code
child_item.item_name = item.item_name
child_item.description = item.description
- child_item.reqd_by_date = p_doc.delivery_date
+ child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.uom = item.stock_uom
- child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0
+ child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
+ if not child_item.warehouse:
+ frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
+ .format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
return child_item
-def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code):
+def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
"""
Returns a Purchase Order Item child item containing the default values
"""
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname)
- item = frappe.get_doc("Item", item_code)
+ item = frappe.get_doc("Item", trans_item.get('item_code'))
child_item.item_code = item.item_code
child_item.item_name = item.item_name
child_item.description = item.description
- child_item.schedule_date = p_doc.schedule_date
+ child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date
child_item.uom = item.stock_uom
- child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0
+ child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item
@@ -1196,9 +1199,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if not d.get("docname"):
new_child_flag = True
if parent_doctype == "Sales Order":
- child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code"))
+ child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d)
if parent_doctype == "Purchase Order":
- child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code"))
+ child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d)
else:
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")):
@@ -1243,6 +1246,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:
+ parent.load_from_db()
child_item.idx = len(parent.items) + 1
child_item.insert()
else:
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index ff07b59bd9b..70f9033f431 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -100,7 +100,7 @@ class BuyingController(StockController):
for d in tax_for_valuation:
d.category = 'Total'
msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items'))
-
+
def validate_asset_return(self):
if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return:
return
@@ -663,10 +663,10 @@ class BuyingController(StockController):
for qty in range(cint(d.qty)):
asset = self.make_asset(d)
created_assets.append(asset)
-
+
if len(created_assets) > 5:
# dont show asset form links if more than 5 assets are created
- messages.append(_('{} Asset{} created for {}').format(len(created_assets), is_plural, frappe.bold(d.item_code)))
+ messages.append(_('{} Assets created for {}').format(len(created_assets), frappe.bold(d.item_code)))
else:
assets_link = list(map(lambda d: frappe.utils.get_link_to_form('Asset', d), created_assets))
assets_link = frappe.bold(','.join(assets_link))
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 163ef72ee10..c58dd7a8e36 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -364,6 +364,19 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
fields = ["name", "parent_account"],
limit_start=start, limit_page_length=page_len, as_list=True)
+def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
+ return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date
+ from `tabBlanket Order` bo, `tabBlanket Order Item` boi
+ where
+ boi.parent = bo.name
+ and boi.item_code = {item_code}
+ and bo.blanket_order_type = '{blanket_order_type}'
+ and bo.company = {company}
+ and bo.docstatus = 1"""
+ .format(item_code = frappe.db.escape(filters.get("item")),
+ blanket_order_type = filters.get("blanket_order_type"),
+ company = frappe.db.escape(filters.get("company"))
+ ))
@frappe.whitelist()
def get_income_account(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 9a9f3d1d319..c25ad060674 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -46,6 +46,7 @@ class SellingController(StockController):
set_default_income_account_for_item(self)
self.set_customer_address()
self.validate_for_duplicate_items()
+ self.validate_target_warehouse()
def set_missing_values(self, for_validate=False):
@@ -164,9 +165,9 @@ class SellingController(StockController):
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
def validate_selling_price(self):
- def throw_message(item_name, rate, ref_rate_field):
- frappe.throw(_("""Selling rate for item {0} is lower than its {1}. Selling rate should be atleast {2}""")
- .format(item_name, ref_rate_field, rate))
+ def throw_message(idx, item_name, rate, ref_rate_field):
+ frappe.throw(_("""Row #{}: Selling rate for item {} is lower than its {}. Selling rate should be atleast {}""")
+ .format(idx, item_name, ref_rate_field, rate))
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
return
@@ -181,7 +182,7 @@ class SellingController(StockController):
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1)
if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom):
- throw_message(it.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
+ throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
last_valuation_rate = frappe.db.sql("""
SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s
@@ -191,7 +192,7 @@ class SellingController(StockController):
if last_valuation_rate:
last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1)
if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom):
- throw_message(it.name, last_valuation_rate_in_sales_uom, "valuation rate")
+ throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")
def get_item_list(self):
@@ -402,6 +403,14 @@ class SellingController(StockController):
else:
chk_dupl_itm.append(f)
+ def validate_target_warehouse(self):
+ items = self.get("items") + (self.get("packed_items") or [])
+
+ for d in items:
+ if d.get("target_warehouse") and d.get("warehouse") == d.get("target_warehouse"):
+ warehouse = frappe.bold(d.get("target_warehouse"))
+ frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same")
+ .format(d.idx, warehouse, warehouse))
def validate_items(self):
# validate items to see if they have is_sales_item enabled
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 5c09068a035..f5c42b49927 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -374,9 +374,6 @@ class StockController(AccountsController):
# Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
- if d.parenttype in ["Delivery Note", "Sales Invoice"] and d.rate:
- frappe.throw(_("Row #{0}: {1} cannot have {2} as it is a Customer Provided Item")
- .format(d.idx, frappe.bold(d.item_code), frappe.bold("Rate")))
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None):
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index b52a07dbdf0..82f820c4252 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -643,8 +643,7 @@ def get_itemised_tax_breakup_html(doc):
itemised_tax=itemised_tax,
itemised_taxable_amount=itemised_taxable_amount,
tax_accounts=tax_accounts,
- conversion_rate=doc.conversion_rate,
- currency=doc.currency
+ doc=doc
)
)
diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py
index 00a4bd1a322..8f60ecf6219 100644
--- a/erpnext/crm/doctype/email_campaign/email_campaign.py
+++ b/erpnext/crm/doctype/email_campaign/email_campaign.py
@@ -27,7 +27,7 @@ class EmailCampaign(Document):
for entry in campaign.get("campaign_schedules"):
send_after_days.append(entry.send_after_days)
try:
- end_date = add_days(getdate(self.start_date), max(send_after_days))
+ self.end_date = add_days(getdate(self.start_date), max(send_after_days))
except ValueError:
frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name))
diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py
new file mode 100644
index 00000000000..38bf79e5fcb
--- /dev/null
+++ b/erpnext/crm/utils.py
@@ -0,0 +1,24 @@
+import frappe
+
+
+def update_lead_phone_numbers(contact, method):
+ if contact.phone_nos:
+ contact_lead = contact.get_link_for("Lead")
+ if contact_lead:
+ phone = mobile_no = contact.phone_nos[0].phone
+
+ if len(contact.phone_nos) > 1:
+ # get the default phone number
+ primary_phones = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_phone]
+ if primary_phones:
+ phone = primary_phones[0]
+
+ # get the default mobile number
+ primary_mobile_nos = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_mobile_no]
+ if primary_mobile_nos:
+ mobile_no = primary_mobile_nos[0]
+
+ lead = frappe.get_doc("Lead", contact_lead)
+ lead.phone = phone
+ lead.mobile_no = mobile_no
+ lead.save()
diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js
index e2c6f1d8565..17ef44954b1 100644
--- a/erpnext/education/doctype/fees/fees.js
+++ b/erpnext/education/doctype/fees/fees.js
@@ -112,6 +112,8 @@ frappe.ui.form.on("Fees", {
args: {
"dt": frm.doc.doctype,
"dn": frm.doc.name,
+ "party_type": "Student",
+ "party": frm.doc.student,
"recipient_id": frm.doc.student_email
},
callback: function(r) {
diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py
index aa616e6206a..f31003bf326 100644
--- a/erpnext/education/doctype/fees/fees.py
+++ b/erpnext/education/doctype/fees/fees.py
@@ -75,7 +75,8 @@ class Fees(AccountsController):
self.make_gl_entries()
if self.send_payment_request and self.student_email:
- pr = make_payment_request(dt="Fees", dn=self.name, recipient_id=self.student_email,
+ pr = make_payment_request(party_type="Student", party=self.student, dt="Fees",
+ dn=self.name, recipient_id=self.student_email,
submit_doc=True, use_dummy_message=True)
frappe.msgprint(_("Payment request {0} created").format(getlink("Payment Request", pr.name)))
diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
index 4422d23e380..618865200cf 100644
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
@@ -144,6 +144,10 @@ def create_sales_order(order, woocommerce_settings, customer_name, sys_lang):
def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang):
company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr')
+ default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr)
+ if not frappe.db.exists("Warehouse", default_warehouse):
+ frappe.throw(_("Please set Warehouse in Woocommerce Settings"))
+
for item in order.get("line_items"):
woocomm_item_id = item.get("product_id")
found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id})
@@ -158,7 +162,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l
"uom": woocommerce_settings.uom or _("Nos", sys_lang),
"qty": item.get("quantity"),
"rate": item.get("price"),
- "warehouse": woocommerce_settings.warehouse or _("Stores - {0}", sys_lang).format(company_abbr)
+ "warehouse": woocommerce_settings.warehouse or default_warehouse
})
add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account)
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
index 104ac570c69..d84c8234efa 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
@@ -2,15 +2,40 @@
// For license information, please see license.txt
frappe.ui.form.on('Tally Migration', {
- onload: function(frm) {
+ onload: function (frm) {
+ let reload_status = true;
frappe.realtime.on("tally_migration_progress_update", function (data) {
+ if (reload_status) {
+ frappe.model.with_doc(frm.doc.doctype, frm.doc.name, () => {
+ frm.refresh_header();
+ });
+ reload_status = false;
+ }
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
- if (data.count == data.total) {
- window.setTimeout(title => frm.dashboard.hide_progress(title), 1500, data.title);
+ let error_occurred = data.count === -1;
+ if (data.count == data.total || error_occurred) {
+ window.setTimeout((title) => {
+ frm.dashboard.hide_progress(title);
+ frm.reload_doc();
+ if (error_occurred) {
+ frappe.msgprint({
+ message: __("An error has occurred during {0}. Check {1} for more details",
+ [
+ repl("%(tally_document)s", {
+ tally_document: frm.docname
+ }),
+ "Error Log"
+ ]
+ ),
+ title: __("Tally Migration Error"),
+ indicator: "red"
+ });
+ }
+ }, 2000, data.title);
}
});
},
- refresh: function(frm) {
+ refresh: function (frm) {
if (frm.doc.master_data && !frm.doc.is_master_data_imported) {
if (frm.doc.is_master_data_processed) {
if (frm.doc.status != "Importing Master Data") {
@@ -34,17 +59,17 @@ frappe.ui.form.on('Tally Migration', {
}
}
},
- add_button: function(frm, label, method) {
+ add_button: function (frm, label, method) {
frm.add_custom_button(
label,
- () => frm.call({
- doc: frm.doc,
- method: method,
- freeze: true,
- callback: () => {
- frm.remove_custom_button(label);
- }
- })
+ () => {
+ frm.call({
+ doc: frm.doc,
+ method: method,
+ freeze: true
+ });
+ frm.reload_doc();
+ }
);
}
});
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
index 26415caf8d3..dc6f093ac9d 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"beta": 1,
"creation": "2019-02-01 14:27:09.485238",
"doctype": "DocType",
@@ -14,6 +15,7 @@
"tally_debtors_account",
"company_section",
"tally_company",
+ "default_uom",
"column_break_8",
"erpnext_company",
"processed_files_section",
@@ -43,6 +45,7 @@
"label": "Status"
},
{
+ "description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs",
"fieldname": "master_data",
"fieldtype": "Attach",
"in_list_view": 1,
@@ -50,6 +53,7 @@
},
{
"default": "Sundry Creditors",
+ "description": "Creditors Account set in Tally",
"fieldname": "tally_creditors_account",
"fieldtype": "Data",
"label": "Tally Creditors Account",
@@ -61,6 +65,7 @@
},
{
"default": "Sundry Debtors",
+ "description": "Debtors Account set in Tally",
"fieldname": "tally_debtors_account",
"fieldtype": "Data",
"label": "Tally Debtors Account",
@@ -72,6 +77,7 @@
"fieldtype": "Section Break"
},
{
+ "description": "Company Name as per Imported Tally Data",
"fieldname": "tally_company",
"fieldtype": "Data",
"label": "Tally Company",
@@ -82,9 +88,11 @@
"fieldtype": "Column Break"
},
{
+ "description": "Your Company set in ERPNext",
"fieldname": "erpnext_company",
"fieldtype": "Data",
- "label": "ERPNext Company"
+ "label": "ERPNext Company",
+ "read_only_depends_on": "eval:doc.is_master_data_processed == 1"
},
{
"fieldname": "processed_files_section",
@@ -155,24 +163,28 @@
"options": "Cost Center"
},
{
+ "default": "0",
"fieldname": "is_master_data_processed",
"fieldtype": "Check",
"label": "Is Master Data Processed",
"read_only": 1
},
{
+ "default": "0",
"fieldname": "is_day_book_data_processed",
"fieldtype": "Check",
"label": "Is Day Book Data Processed",
"read_only": 1
},
{
+ "default": "0",
"fieldname": "is_day_book_data_imported",
"fieldtype": "Check",
"label": "Is Day Book Data Imported",
"read_only": 1
},
{
+ "default": "0",
"fieldname": "is_master_data_imported",
"fieldtype": "Check",
"label": "Is Master Data Imported",
@@ -188,13 +200,23 @@
"fieldtype": "Column Break"
},
{
+ "description": "Day Book Data exported from Tally that consists of all historic transactions",
"fieldname": "day_book_data",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Day Book Data"
+ },
+ {
+ "default": "Unit",
+ "description": "UOM in case unspecified in imported data",
+ "fieldname": "default_uom",
+ "fieldtype": "Link",
+ "label": "Default UOM",
+ "options": "UOM"
}
],
- "modified": "2019-04-29 05:46:54.394967",
+ "links": [],
+ "modified": "2020-04-16 13:03:28.894919",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Tally Migration",
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 01eee5b61f2..13474e19ee1 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -4,20 +4,23 @@
from __future__ import unicode_literals
-from decimal import Decimal
import json
import re
import traceback
import zipfile
+from decimal import Decimal
+
+from bs4 import BeautifulSoup as bs
+
import frappe
+from erpnext import encode_company_abbr
+from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.document import Document
from frappe.model.naming import getseries, revert_series_if_last
from frappe.utils.data import format_datetime
-from bs4 import BeautifulSoup as bs
-from erpnext import encode_company_abbr
-from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
+
PRIMARY_ACCOUNT = "Primary"
VOUCHER_CHUNK_SIZE = 500
@@ -39,13 +42,15 @@ class TallyMigration(Document):
return string
master_file = frappe.get_doc("File", {"file_url": data_file})
+ master_file_path = master_file.get_full_path()
- with zipfile.ZipFile(master_file.get_full_path()) as zf:
- encoded_content = zf.read(zf.namelist()[0])
- try:
- content = encoded_content.decode("utf-8-sig")
- except UnicodeDecodeError:
- content = encoded_content.decode("utf-16")
+ if zipfile.is_zipfile(master_file_path):
+ with zipfile.ZipFile(master_file_path) as zf:
+ encoded_content = zf.read(zf.namelist()[0])
+ try:
+ content = encoded_content.decode("utf-8-sig")
+ except UnicodeDecodeError:
+ content = encoded_content.decode("utf-16")
master = bs(sanitize(emptify(content)), "xml")
collection = master.BODY.IMPORTDATA.REQUESTDATA
@@ -58,13 +63,14 @@ class TallyMigration(Document):
"file_name": key + ".json",
"attached_to_doctype": self.doctype,
"attached_to_name": self.name,
- "content": json.dumps(value)
+ "content": json.dumps(value),
+ "is_private": True
}).insert()
setattr(self, key, f.file_url)
def _process_master_data(self):
def get_company_name(collection):
- return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string
+ return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip()
def get_coa_customers_suppliers(collection):
root_type_map = {
@@ -97,17 +103,17 @@ class TallyMigration(Document):
# If Ledger doesn't have PARENT field then don't create Account
# For example "Profit & Loss A/c"
if account.PARENT:
- yield account.PARENT.string, account["NAME"], 0
+ yield account.PARENT.string.strip(), account["NAME"], 0
def get_parent(account):
if account.PARENT:
- return account.PARENT.string
+ return account.PARENT.string.strip()
return {
("Yes", "No"): "Application of Funds (Assets)",
("Yes", "Yes"): "Expenses",
("No", "Yes"): "Income",
("No", "No"): "Source of Funds (Liabilities)",
- }[(account.ISDEEMEDPOSITIVE.string, account.ISREVENUE.string)]
+ }[(account.ISDEEMEDPOSITIVE.string.strip(), account.ISREVENUE.string.strip())]
def get_children_and_parent_dict(accounts):
children, parents = {}, {}
@@ -145,38 +151,38 @@ class TallyMigration(Document):
parties, addresses = [], []
for account in collection.find_all("LEDGER"):
party_type = None
- if account.NAME.string in customers:
+ if account.NAME.string.strip() in customers:
party_type = "Customer"
parties.append({
"doctype": party_type,
- "customer_name": account.NAME.string,
- "tax_id": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None,
+ "customer_name": account.NAME.string.strip(),
+ "tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None,
"customer_group": "All Customer Groups",
"territory": "All Territories",
"customer_type": "Individual",
})
- elif account.NAME.string in suppliers:
+ elif account.NAME.string.strip() in suppliers:
party_type = "Supplier"
parties.append({
"doctype": party_type,
- "supplier_name": account.NAME.string,
- "pan": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None,
+ "supplier_name": account.NAME.string.strip(),
+ "pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None,
"supplier_group": "All Supplier Groups",
"supplier_type": "Individual",
})
if party_type:
- address = "\n".join([a.string for a in account.find_all("ADDRESS")])
+ address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")])
addresses.append({
"doctype": "Address",
"address_line1": address[:140].strip(),
"address_line2": address[140:].strip(),
- "country": account.COUNTRYNAME.string if account.COUNTRYNAME else None,
- "state": account.LEDSTATENAME.string if account.LEDSTATENAME else None,
- "gst_state": account.LEDSTATENAME.string if account.LEDSTATENAME else None,
- "pin_code": account.PINCODE.string if account.PINCODE else None,
- "mobile": account.LEDGERPHONE.string if account.LEDGERPHONE else None,
- "phone": account.LEDGERPHONE.string if account.LEDGERPHONE else None,
- "gstin": account.PARTYGSTIN.string if account.PARTYGSTIN else None,
+ "country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None,
+ "state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
+ "gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
+ "pin_code": account.PINCODE.string.strip() if account.PINCODE else None,
+ "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
+ "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
+ "gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None,
"links": [{"link_doctype": party_type, "link_name": account["NAME"]}],
})
return parties, addresses
@@ -184,41 +190,50 @@ class TallyMigration(Document):
def get_stock_items_uoms(collection):
uoms = []
for uom in collection.find_all("UNIT"):
- uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string})
+ uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string.strip()})
items = []
for item in collection.find_all("STOCKITEM"):
+ stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom
items.append({
"doctype": "Item",
- "item_code" : item.NAME.string,
- "stock_uom": item.BASEUNITS.string,
+ "item_code" : item.NAME.string.strip(),
+ "stock_uom": stock_uom.strip(),
"is_stock_item": 0,
"item_group": "All Item Groups",
"item_defaults": [{"company": self.erpnext_company}]
})
+
return items, uoms
+ try:
+ self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
+ collection = self.get_collection(self.master_data)
+ company = get_company_name(collection)
+ self.tally_company = company
+ self.erpnext_company = company
- self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
- collection = self.get_collection(self.master_data)
+ self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
+ chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
- company = get_company_name(collection)
- self.tally_company = company
- self.erpnext_company = company
+ self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
+ parties, addresses = get_parties_addresses(collection, customers, suppliers)
- self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
- chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
- self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
- parties, addresses = get_parties_addresses(collection, customers, suppliers)
- self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5)
- items, uoms = get_stock_items_uoms(collection)
- data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms}
- self.publish("Process Master Data", _("Done"), 5, 5)
+ self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5)
+ items, uoms = get_stock_items_uoms(collection)
+ data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms}
- self.dump_processed_data(data)
- self.is_master_data_processed = 1
- self.status = ""
- self.save()
+ self.publish("Process Master Data", _("Done"), 5, 5)
+ self.dump_processed_data(data)
+
+ self.is_master_data_processed = 1
+
+ except:
+ self.publish("Process Master Data", _("Process Failed"), -1, 5)
+ self.log()
+
+ finally:
+ self.set_status()
def publish(self, title, message, count, total):
frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total})
@@ -256,7 +271,6 @@ class TallyMigration(Document):
except:
self.log(address)
-
def create_items_uoms(items_file_url, uoms_file_url):
uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url})
for uom in json.loads(uoms_file.get_content()):
@@ -273,25 +287,35 @@ class TallyMigration(Document):
except:
self.log(item)
- self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
- create_company_and_coa(self.chart_of_accounts)
- self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
- create_parties_and_addresses(self.parties, self.addresses)
- self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
- create_items_uoms(self.items, self.uoms)
- self.publish("Import Master Data", _("Done"), 4, 4)
- self.status = ""
- self.is_master_data_imported = 1
- self.save()
+ try:
+ self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
+ create_company_and_coa(self.chart_of_accounts)
+
+ self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
+ create_parties_and_addresses(self.parties, self.addresses)
+
+ self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
+ create_items_uoms(self.items, self.uoms)
+
+ self.publish("Import Master Data", _("Done"), 4, 4)
+
+ self.is_master_data_imported = 1
+
+ except:
+ self.publish("Import Master Data", _("Process Failed"), -1, 5)
+ self.log()
+
+ finally:
+ self.set_status()
def _process_day_book_data(self):
def get_vouchers(collection):
vouchers = []
for voucher in collection.find_all("VOUCHER"):
- if voucher.ISCANCELLED.string == "Yes":
+ if voucher.ISCANCELLED.string.strip() == "Yes":
continue
inventory_entries = voucher.find_all("INVENTORYENTRIES.LIST") + voucher.find_all("ALLINVENTORYENTRIES.LIST") + voucher.find_all("INVENTORYENTRIESIN.LIST") + voucher.find_all("INVENTORYENTRIESOUT.LIST")
- if voucher.VOUCHERTYPENAME.string not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries:
+ if voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries:
function = voucher_to_invoice
else:
function = voucher_to_journal_entry
@@ -307,15 +331,15 @@ class TallyMigration(Document):
accounts = []
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST")
for entry in ledger_entries:
- account = {"account": encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company), "cost_center": self.default_cost_center}
- if entry.ISPARTYLEDGER.string == "Yes":
- party_details = get_party(entry.LEDGERNAME.string)
+ account = {"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company), "cost_center": self.default_cost_center}
+ if entry.ISPARTYLEDGER.string.strip() == "Yes":
+ party_details = get_party(entry.LEDGERNAME.string.strip())
if party_details:
party_type, party_account = party_details
account["party_type"] = party_type
account["account"] = party_account
- account["party"] = entry.LEDGERNAME.string
- amount = Decimal(entry.AMOUNT.string)
+ account["party"] = entry.LEDGERNAME.string.strip()
+ amount = Decimal(entry.AMOUNT.string.strip())
if amount > 0:
account["credit_in_account_currency"] = str(abs(amount))
else:
@@ -324,21 +348,21 @@ class TallyMigration(Document):
journal_entry = {
"doctype": "Journal Entry",
- "tally_guid": voucher.GUID.string,
- "posting_date": voucher.DATE.string,
+ "tally_guid": voucher.GUID.string.strip(),
+ "posting_date": voucher.DATE.string.strip(),
"company": self.erpnext_company,
"accounts": accounts,
}
return journal_entry
def voucher_to_invoice(voucher):
- if voucher.VOUCHERTYPENAME.string in ["Sales", "Credit Note"]:
+ if voucher.VOUCHERTYPENAME.string.strip() in ["Sales", "Credit Note"]:
doctype = "Sales Invoice"
party_field = "customer"
account_field = "debit_to"
account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
price_list_field = "selling_price_list"
- elif voucher.VOUCHERTYPENAME.string in ["Purchase", "Debit Note"]:
+ elif voucher.VOUCHERTYPENAME.string.strip() in ["Purchase", "Debit Note"]:
doctype = "Purchase Invoice"
party_field = "supplier"
account_field = "credit_to"
@@ -351,10 +375,10 @@ class TallyMigration(Document):
invoice = {
"doctype": doctype,
- party_field: voucher.PARTYNAME.string,
- "tally_guid": voucher.GUID.string,
- "posting_date": voucher.DATE.string,
- "due_date": voucher.DATE.string,
+ party_field: voucher.PARTYNAME.string.strip(),
+ "tally_guid": voucher.GUID.string.strip(),
+ "posting_date": voucher.DATE.string.strip(),
+ "due_date": voucher.DATE.string.strip(),
"items": get_voucher_items(voucher, doctype),
"taxes": get_voucher_taxes(voucher),
account_field: account_name,
@@ -375,15 +399,15 @@ class TallyMigration(Document):
for entry in inventory_entries:
qty, uom = entry.ACTUALQTY.string.strip().split()
items.append({
- "item_code": entry.STOCKITEMNAME.string,
- "description": entry.STOCKITEMNAME.string,
+ "item_code": entry.STOCKITEMNAME.string.strip(),
+ "description": entry.STOCKITEMNAME.string.strip(),
"qty": qty.strip(),
"uom": uom.strip(),
"conversion_factor": 1,
- "price_list_rate": entry.RATE.string.split("/")[0],
+ "price_list_rate": entry.RATE.string.strip().split("/")[0],
"cost_center": self.default_cost_center,
"warehouse": self.default_warehouse,
- account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string, self.erpnext_company),
+ account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(), self.erpnext_company),
})
return items
@@ -391,13 +415,13 @@ class TallyMigration(Document):
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST")
taxes = []
for entry in ledger_entries:
- if entry.ISPARTYLEDGER.string == "No":
- tax_account = encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company)
+ if entry.ISPARTYLEDGER.string.strip() == "No":
+ tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company)
taxes.append({
"charge_type": "Actual",
"account_head": tax_account,
"description": tax_account,
- "tax_amount": entry.AMOUNT.string,
+ "tax_amount": entry.AMOUNT.string.strip(),
"cost_center": self.default_cost_center,
})
return taxes
@@ -408,15 +432,24 @@ class TallyMigration(Document):
elif frappe.db.exists({"doctype": "Customer", "customer_name": party}):
return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
- self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
- collection = self.get_collection(self.day_book_data)
- self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
- vouchers = get_vouchers(collection)
- self.publish("Process Day Book Data", _("Done"), 3, 3)
- self.dump_processed_data({"vouchers": vouchers})
- self.status = ""
- self.is_day_book_data_processed = 1
- self.save()
+ try:
+ self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
+ collection = self.get_collection(self.day_book_data)
+
+ self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
+ vouchers = get_vouchers(collection)
+
+ self.publish("Process Day Book Data", _("Done"), 3, 3)
+ self.dump_processed_data({"vouchers": vouchers})
+
+ self.is_day_book_data_processed = 1
+
+ except:
+ self.publish("Process Day Book Data", _("Process Failed"), -1, 5)
+ self.log()
+
+ finally:
+ self.set_status()
def _import_day_book_data(self):
def create_fiscal_years(vouchers):
@@ -454,23 +487,31 @@ class TallyMigration(Document):
"currency": "INR"
}).insert()
- frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable")
- frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable")
- frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account)
+ try:
+ frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable")
+ frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable")
+ frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account)
- vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
- vouchers = json.loads(vouchers_file.get_content())
+ vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
+ vouchers = json.loads(vouchers_file.get_content())
- create_fiscal_years(vouchers)
- create_price_list()
- create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"])
+ create_fiscal_years(vouchers)
+ create_price_list()
+ create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"])
- total = len(vouchers)
- is_last = False
- for index in range(0, total, VOUCHER_CHUNK_SIZE):
- if index + VOUCHER_CHUNK_SIZE >= total:
- is_last = True
- frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last)
+ total = len(vouchers)
+ is_last = False
+
+ for index in range(0, total, VOUCHER_CHUNK_SIZE):
+ if index + VOUCHER_CHUNK_SIZE >= total:
+ is_last = True
+ frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last)
+
+ except:
+ self.log()
+
+ finally:
+ self.set_status()
def _import_vouchers(self, start, total, is_last=False):
frappe.flags.in_migrate = True
@@ -494,25 +535,26 @@ class TallyMigration(Document):
frappe.flags.in_migrate = False
def process_master_data(self):
- self.status = "Processing Master Data"
- self.save()
+ self.set_status("Processing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
def import_master_data(self):
- self.status = "Importing Master Data"
- self.save()
+ self.set_status("Importing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
def process_day_book_data(self):
- self.status = "Processing Day Book Data"
- self.save()
+ self.set_status("Processing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
def import_day_book_data(self):
- self.status = "Importing Day Book Data"
- self.save()
+ self.set_status("Importing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
def log(self, data=None):
- message = "\n".join(["Data", json.dumps(data, default=str, indent=4), "Exception", traceback.format_exc()])
+ data = data or self.status
+ message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
return frappe.log_error(title="Tally Migration Error", message=message)
+
+ def set_status(self, status=""):
+ self.status = status
+ self.save()
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
index 7c6f4d5999e..a98ae25e43b 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
@@ -158,7 +158,7 @@ def get_item_dict(table, parent, parenttype):
def create_stock_entry(doc):
stock_entry = frappe.new_doc("Stock Entry")
stock_entry = set_stock_items(stock_entry, doc.name, "Clinical Procedure")
- stock_entry.purpose = "Material Issue"
+ stock_entry.stock_entry_type = "Material Issue"
stock_entry.from_warehouse = doc.warehouse
stock_entry.company = doc.company
expense_account = get_account(None, "expense_account", "Healthcare Settings", doc.company)
diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py
index 00fad910a4f..69237847986 100644
--- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py
+++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py
@@ -78,6 +78,8 @@ def create_item_from_template(doc):
if doc.is_billable:
disabled = 0
+ uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom')
+
#insert item
item = frappe.get_doc({
'doctype': 'Item',
@@ -92,7 +94,7 @@ def create_item_from_template(doc):
'show_in_website': 0,
'is_pro_applicable': 0,
'disabled': disabled,
- 'stock_uom': 'Unit'
+ 'stock_uom': uom
}).insert(ignore_permissions=True)
#insert item price
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index 8f3602af67e..9bdba032e4a 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -69,12 +69,13 @@ class PatientAppointment(Document):
if fee_validity.ref_invoice:
frappe.db.set_value("Patient Appointment", appointment.name, "invoiced", True)
frappe.msgprint(_("{0} has fee validity till {1}").format(appointment.patient, fee_validity.valid_till))
- confirm_sms(self)
if frappe.db.get_value("Healthcare Settings", None, "manage_appointment_invoice_automatically") == '1' and \
frappe.db.get_value("Patient Appointment", self.name, "invoiced") != 1:
invoice_appointment(self)
+ send_confirmation_msg(self)
+
@frappe.whitelist()
def invoice_appointment(appointment_doc):
if not appointment_doc.name:
@@ -303,10 +304,14 @@ def set_pending_appointments():
"('Scheduled','Open') and appointment_date < %s", today)
-def confirm_sms(doc):
- if frappe.db.get_value("Healthcare Settings", None, "app_con") == '1':
- message = frappe.db.get_value("Healthcare Settings", None, "app_con_msg")
- send_message(doc, message)
+def send_confirmation_msg(doc):
+ if frappe.db.get_single_value("Healthcare Settings", "app_con"):
+ message = frappe.db.get_single_value("Healthcare Settings", "app_con_msg")
+ try:
+ send_message(doc, message)
+ except Exception:
+ frappe.log_error(frappe.get_traceback(), _("Appointment Confirmation Message Not Sent"))
+ frappe.msgprint(_("Appointment Confirmation Message Not Sent"), indicator="orange")
@frappe.whitelist()
def create_encounter(appointment):
@@ -352,7 +357,7 @@ def send_message(doc, message):
# jinja to string convertion happens here
message = frappe.render_template(message, context)
- number = [patient.mobile]
+ number = [patient_mobile]
send_sms(number, message)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index da180f86884..6712842948e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -256,7 +256,8 @@ doc_events = {
},
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
- "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
+ "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information",
+ "validate": "erpnext.crm.utils.update_lead_phone_numbers"
},
"Lead": {
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information"
@@ -269,8 +270,7 @@ doc_events = {
scheduler_events = {
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
- "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder"
-
+ "erpnext.healthcare.doctype.patient_appointment.patient_appointment.set_appointment_reminder"
],
"hourly": [
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
@@ -530,4 +530,4 @@ global_search_doctypes = {
{'doctype': 'Hotel Room Package', 'index': 3},
{'doctype': 'Hotel Room Type', 'index': 4}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index f10e3b6ce26..f0663aefa83 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -136,9 +136,18 @@ def make_bank_entry(dt, dn):
def make_return_entry(employee, company, employee_advance_name,
return_amount, advance_account, mode_of_payment=None):
return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
+
+ mode_of_payment_type = ''
+ if mode_of_payment:
+ mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type')
+ if mode_of_payment_type not in ["Cash", "Bank"]:
+ # if mode of payment is General then it unset the type
+ mode_of_payment_type = None
+
je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate()
- je.voucher_type = 'Bank Entry'
+ # if mode of payment is Bank then voucher type is Bank Entry
+ je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry'
je.company = company
je.remark = 'Return against Employee Advance: ' + employee_advance_name
diff --git a/erpnext/hr/doctype/employee_other_income/__init__.py b/erpnext/hr/doctype/employee_other_income/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.js b/erpnext/hr/doctype/employee_other_income/employee_other_income.js
new file mode 100644
index 00000000000..c1a74e863ba
--- /dev/null
+++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Employee Other Income', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json
new file mode 100644
index 00000000000..2dd6c10988d
--- /dev/null
+++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json
@@ -0,0 +1,138 @@
+{
+ "actions": [],
+ "autoname": "HR-INCOME-.######",
+ "creation": "2020-03-18 15:04:40.767434",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "payroll_period",
+ "column_break_3",
+ "company",
+ "source",
+ "amount",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1
+ },
+ {
+ "fieldname": "payroll_period",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payroll Period",
+ "options": "Payroll Period",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "source",
+ "fieldtype": "Data",
+ "label": "Source"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "Company:company:default_currency",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Employee Other Income",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-03-19 18:06:45.361830",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Other Income",
+ "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": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "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/hr/doctype/employee_other_income/employee_other_income.py b/erpnext/hr/doctype/employee_other_income/employee_other_income.py
new file mode 100644
index 00000000000..ab63c0de623
--- /dev/null
+++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class EmployeeOtherIncome(Document):
+ pass
diff --git a/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py
new file mode 100644
index 00000000000..2eeca7a23de
--- /dev/null
+++ b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.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 TestEmployeeOtherIncome(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
index e102ff8d705..18fad85c4b3 100644
--- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
+++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
@@ -1,620 +1,180 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "HR-TAX-DEC-.YYYY.-.#####",
- "beta": 0,
- "creation": "2018-04-13 16:53:36.175504",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "HR-TAX-DEC-.YYYY.-.#####",
+ "creation": "2018-04-13 16:53:36.175504",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "department",
+ "column_break_2",
+ "payroll_period",
+ "company",
+ "amended_from",
+ "section_break_8",
+ "declarations",
+ "section_break_10",
+ "total_declared_amount",
+ "column_break_12",
+ "total_exemption_amount"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "employee",
- "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": "Employee",
- "length": 0,
- "no_copy": 0,
- "options": "Employee",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.employee_name",
- "fetch_if_empty": 0,
- "fieldname": "employee_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": "Employee 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
- },
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.department",
- "fetch_if_empty": 0,
- "fieldname": "department",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "payroll_period",
- "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": "Payroll Period",
- "length": 0,
- "no_copy": 0,
- "options": "Payroll Period",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "payroll_period",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payroll Period",
+ "options": "Payroll Period",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.company",
- "fetch_if_empty": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "amended_from",
- "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": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Employee Tax Exemption Declaration",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Employee Tax Exemption Declaration",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_8",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "declarations",
- "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": "Declarations",
- "length": 0,
- "no_copy": 0,
- "options": "Employee Tax Exemption Declaration Category",
- "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": "declarations",
+ "fieldtype": "Table",
+ "label": "Declarations",
+ "options": "Employee Tax Exemption Declaration Category"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_10",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "total_declared_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Declared Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "total_declared_amount",
+ "fieldtype": "Currency",
+ "label": "Total Declared Amount",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_12",
- "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_12",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "total_exemption_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Exemption Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "other_incomes_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": "Other Incomes",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "income_from_other_sources",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Income From Other Sources",
- "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": "total_exemption_amount",
+ "fieldtype": "Currency",
+ "label": "Total Exemption Amount",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-05-11 16:13:50.472670",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Employee Tax Exemption Declaration",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-03-18 14:56:25.625717",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Tax Exemption Declaration",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
- },
+ },
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
- },
+ },
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
"write": 1
- },
+ },
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
index f2bba7afed7..fb71a2877a1 100644
--- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
+++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
@@ -8,31 +8,17 @@ from frappe.model.document import Document
from frappe import _
from frappe.utils import flt
from frappe.model.mapper import get_mapped_doc
-from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_annual_eligible_hra_exemption
-
-class DuplicateDeclarationError(frappe.ValidationError): pass
+from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \
+ calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionDeclaration(Document):
def validate(self):
validate_tax_declaration(self.declarations)
- self.validate_duplicate()
+ validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee)
self.set_total_declared_amount()
self.set_total_exemption_amount()
self.calculate_hra_exemption()
- def validate_duplicate(self):
- duplicate = frappe.db.get_value("Employee Tax Exemption Declaration",
- filters = {
- "employee": self.employee,
- "payroll_period": self.payroll_period,
- "name": ["!=", self.name],
- "docstatus": ["!=", 2]
- }
- )
- if duplicate:
- frappe.throw(_("Duplicate Tax Declaration of {0} for period {1}")
- .format(self.employee, self.payroll_period), DuplicateDeclarationError)
-
def set_total_declared_amount(self):
self.total_declared_amount = 0.0
for d in self.declarations:
diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
index 9c87bbd1f30..9549fd1b757 100644
--- a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
+++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext
import unittest
from erpnext.hr.doctype.employee.test_employee import make_employee
-from erpnext.hr.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration import DuplicateDeclarationError
+from erpnext.hr.utils import DuplicateDeclarationError
class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
def setUp(self):
diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
index 9792bd1db61..e13b1ac8888 100644
--- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
+++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
@@ -1,635 +1,140 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "HR-TAX-PRF-.YYYY.-.#####",
- "beta": 0,
- "creation": "2018-04-13 17:24:11.456132",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "HR-TAX-PRF-.YYYY.-.#####",
+ "creation": "2018-04-13 17:24:11.456132",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "department",
+ "column_break_2",
+ "submission_date",
+ "payroll_period",
+ "company",
+ "section_break_5",
+ "tax_exemption_proofs",
+ "section_break_10",
+ "total_actual_amount",
+ "column_break_12",
+ "exemption_amount",
+ "attachment_section",
+ "attachments",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "employee",
- "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": "Employee",
- "length": 0,
- "no_copy": 0,
- "options": "Employee",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.employee_name",
- "fetch_if_empty": 0,
- "fieldname": "employee_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": "Employee 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
- },
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.department",
- "fetch_if_empty": 0,
- "fieldname": "department",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fetch_if_empty": 0,
- "fieldname": "submission_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Submission Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "Today",
+ "fieldname": "submission_date",
+ "fieldtype": "Date",
+ "label": "Submission Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "payroll_period",
- "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": "Payroll Period",
- "length": 0,
- "no_copy": 0,
- "options": "Payroll Period",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "payroll_period",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Payroll Period",
+ "options": "Payroll Period",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.company",
- "fetch_if_empty": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "tax_exemption_proofs",
- "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": "Tax Exemption Proofs",
- "length": 0,
- "no_copy": 0,
- "options": "Employee Tax Exemption Proof Submission Detail",
- "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": "tax_exemption_proofs",
+ "fieldtype": "Table",
+ "label": "Tax Exemption Proofs",
+ "options": "Employee Tax Exemption Proof Submission Detail"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_10",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "total_actual_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Actual Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "total_actual_amount",
+ "fieldtype": "Currency",
+ "label": "Total Actual Amount",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_12",
- "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_12",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "exemption_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Total Exemption Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "exemption_amount",
+ "fieldtype": "Currency",
+ "label": "Total Exemption Amount",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "other_incomes_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": "Other Incomes",
- "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": "attachment_section",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "income_from_other_sources",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Income From Other Sources",
- "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": "attachments",
+ "fieldtype": "Attach",
+ "label": "Attachments"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "attachment_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": "",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "attachments",
- "fieldtype": "Attach",
- "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": "Attachments",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "amended_from",
- "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": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Employee Tax Exemption Proof Submission",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Employee Tax Exemption Proof Submission",
+ "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": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-05-13 12:17:18.045171",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Employee Tax Exemption Proof Submission",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-03-18 14:55:51.420016",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Tax Exemption Proof Submission",
+ "owner": "Administrator",
"permissions": [
{
"amend": 1,
diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
index 97ceb63476b..5bc33a65f2c 100644
--- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
+++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
@@ -7,7 +7,8 @@ import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import flt
-from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_hra_exemption_for_period
+from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \
+ calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionProofSubmission(Document):
def validate(self):
@@ -15,6 +16,7 @@ class EmployeeTaxExemptionProofSubmission(Document):
self.set_total_actual_amount()
self.set_total_exemption_amount()
self.calculate_hra_exemption()
+ validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee)
def set_total_actual_amount(self):
self.total_actual_amount = flt(self.get("house_rent_payment_amount"))
@@ -32,4 +34,4 @@ class EmployeeTaxExemptionProofSubmission(Document):
self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
self.monthly_house_rent = hra_exemption["monthly_house_rent"]
- self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
\ No newline at end of file
+ self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
diff --git a/erpnext/hr/doctype/income_tax_slab/__init__.py b/erpnext/hr/doctype/income_tax_slab/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js
new file mode 100644
index 00000000000..73a54eb8dd9
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Income Tax Slab', {
+
+});
diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json
new file mode 100644
index 00000000000..f74315f32e9
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json
@@ -0,0 +1,152 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2020-03-17 16:50:35.564915",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "effective_from",
+ "company",
+ "column_break_3",
+ "allow_tax_exemption",
+ "standard_tax_exemption_amount",
+ "disabled",
+ "amended_from",
+ "taxable_salary_slabs_section",
+ "slabs",
+ "taxes_and_charges_on_income_tax_section",
+ "other_taxes_and_charges"
+ ],
+ "fields": [
+ {
+ "fieldname": "effective_from",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Effective from",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "If enabled, Tax Exemption Declaration will be considered for income tax calculation.",
+ "fieldname": "allow_tax_exemption",
+ "fieldtype": "Check",
+ "label": "Allow Tax Exemption"
+ },
+ {
+ "fieldname": "taxable_salary_slabs_section",
+ "fieldtype": "Section Break",
+ "label": "Taxable Salary Slabs"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Income Tax Slab",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "slabs",
+ "fieldtype": "Table",
+ "label": "Taxable Salary Slabs",
+ "options": "Taxable Salary Slab",
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "depends_on": "allow_tax_exemption",
+ "fieldname": "standard_tax_exemption_amount",
+ "fieldtype": "Currency",
+ "label": "Standard Tax Exemption Amount",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "other_taxes_and_charges",
+ "fieldname": "taxes_and_charges_on_income_tax_section",
+ "fieldtype": "Section Break",
+ "label": "Taxes and Charges on Income Tax"
+ },
+ {
+ "fieldname": "other_taxes_and_charges",
+ "fieldtype": "Table",
+ "label": "Other Taxes and Charges",
+ "options": "Income Tax Slab Other Charges"
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-04-29 15:08:21.436120",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Income Tax Slab",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py
new file mode 100644
index 00000000000..253f023f68b
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class IncomeTaxSlab(Document):
+ pass
diff --git a/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py
new file mode 100644
index 00000000000..deaaf650a96
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.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 TestIncomeTaxSlab(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py b/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json
new file mode 100644
index 00000000000..b23fb3dc317
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json
@@ -0,0 +1,75 @@
+{
+ "actions": [],
+ "creation": "2020-04-24 11:46:59.041180",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "description",
+ "column_break_2",
+ "percent",
+ "conditions_section",
+ "min_taxable_income",
+ "column_break_7",
+ "max_taxable_income"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "min_taxable_income",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Min Taxable Income",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "columns": 4,
+ "fieldname": "description",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Description",
+ "reqd": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "percent",
+ "fieldtype": "Percent",
+ "in_list_view": 1,
+ "label": "Percent",
+ "reqd": 1
+ },
+ {
+ "fieldname": "conditions_section",
+ "fieldtype": "Section Break",
+ "label": "Conditions"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "max_taxable_income",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Max Taxable Income",
+ "options": "Company:company:default_currency"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-04-24 13:27:43.598967",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Income Tax Slab Other Charges",
+ "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/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py
new file mode 100644
index 00000000000..b4098ecbf3e
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class IncomeTaxSlabOtherCharges(Document):
+ pass
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index d13bb4577cd..03fe3fa035c 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -30,16 +30,16 @@ class LeaveAllocation(Document):
def validate_leave_allocation_days(self):
company = frappe.db.get_value("Employee", self.employee, "company")
leave_period = get_leave_period(self.from_date, self.to_date, company)
- max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")
+ max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed"))
if max_leaves_allowed > 0:
leave_allocated = 0
if leave_period:
leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type,
leave_period[0].from_date, leave_period[0].to_date)
- leave_allocated += self.new_leaves_allocated
+ leave_allocated += flt(self.new_leaves_allocated)
if leave_allocated > max_leaves_allowed:
frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")
- .format(self.leave_type, self.employee))
+ .format(self.leave_type, self.employee))
def on_submit(self):
self.create_leave_ledger_entry()
diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.js b/erpnext/hr/doctype/payroll_entry/payroll_entry.js
index d25eb6d7810..da25d7574e0 100644
--- a/erpnext/hr/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.js
@@ -249,7 +249,7 @@ const submit_salary_slip = function (frm) {
let make_bank_entry = function (frm) {
var doc = frm.doc;
- if (doc.company && doc.start_date && doc.end_date && doc.payment_account) {
+ if (doc.payment_account) {
return frappe.call({
doc: cur_frm.doc,
method: "make_payment_entry",
@@ -262,7 +262,8 @@ let make_bank_entry = function (frm) {
freeze_message: __("Creating Payment Entries......")
});
} else {
- frappe.msgprint(__("Company, Payment Account, From Date and To Date is mandatory"));
+ frappe.msgprint(__("Payment Account is mandatory"));
+ frm.scroll_to_field('payment_account');
}
};
diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.json b/erpnext/hr/doctype/payroll_period/payroll_period.json
index c9bac095f9f..c0fa506e7f0 100644
--- a/erpnext/hr/doctype/payroll_period/payroll_period.json
+++ b/erpnext/hr/doctype/payroll_period/payroll_period.json
@@ -1,401 +1,102 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2018-04-13 15:18:53.698553",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "Prompt",
+ "creation": "2018-04-13 15:18:53.698553",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "column_break_2",
+ "start_date",
+ "end_date",
+ "section_break_5",
+ "periods"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Start Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Start Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "label": "End Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payroll Periods",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Payroll Periods"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "periods",
- "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": "Payroll Periods",
- "length": 0,
- "no_copy": 0,
- "options": "Payroll Period Date",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_7",
- "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": "Taxable Salary Slabs",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "taxable_salary_slabs",
- "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": "Taxable Salary Slabs",
- "length": 0,
- "no_copy": 0,
- "options": "Taxable Salary Slab",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "standard_tax_exemption_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Standard Tax Exemption Amount",
- "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": "periods",
+ "fieldtype": "Table",
+ "label": "Payroll Periods",
+ "options": "Payroll Period Date"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-04-26 01:45:03.160929",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Payroll Period",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2020-03-18 18:13:23.859980",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Payroll Period",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.py b/erpnext/hr/doctype/payroll_period/payroll_period.py
index c1769591eaf..6956c382854 100644
--- a/erpnext/hr/doctype/payroll_period/payroll_period.py
+++ b/erpnext/hr/doctype/payroll_period/payroll_period.py
@@ -45,8 +45,9 @@ class PayrollPeriod(Document):
+ _(") for {0}").format(self.company)
frappe.throw(msg)
-def get_payroll_period_days(start_date, end_date, employee):
- company = frappe.db.get_value("Employee", employee, "company")
+def get_payroll_period_days(start_date, end_date, employee, company=None):
+ if not company:
+ company = frappe.db.get_value("Employee", employee, "company")
payroll_period = frappe.db.sql("""
select name, start_date, end_date
from `tabPayroll Period`
diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json
index 986030d8c58..97c46c829e7 100644
--- a/erpnext/hr/doctype/salary_component/salary_component.json
+++ b/erpnext/hr/doctype/salary_component/salary_component.json
@@ -1,264 +1,263 @@
{
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:salary_component",
- "creation": "2016-06-30 15:42:43.631931",
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "salary_component",
- "salary_component_abbr",
- "type",
- "description",
- "column_break_4",
- "is_payable",
- "depends_on_payment_days",
- "is_tax_applicable",
- "deduct_full_tax_on_selected_payroll_date",
- "round_to_the_nearest_integer",
- "statistical_component",
- "do_not_include_in_total",
- "disabled",
- "flexible_benefits",
- "is_flexible_benefit",
- "max_benefit_amount",
- "column_break_9",
- "pay_against_benefit_claim",
- "only_tax_impact",
- "create_separate_payment_entry_against_benefit_claim",
- "section_break_11",
- "variable_based_on_taxable_salary",
- "section_break_5",
- "accounts",
- "condition_and_formula",
- "condition",
- "amount",
- "amount_based_on_formula",
- "formula",
- "column_break_28",
- "help"
- ],
- "fields": [
- {
- "fieldname": "salary_component",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Name",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "salary_component_abbr",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Abbr",
- "print_width": "120px",
- "reqd": 1,
- "width": "120px"
- },
- {
- "fieldname": "type",
- "fieldtype": "Select",
- "in_standard_filter": 1,
- "label": "Type",
- "options": "Earning\nDeduction",
- "reqd": 1
- },
- {
- "default": "1",
- "depends_on": "eval:doc.type == \"Earning\"",
- "fieldname": "is_tax_applicable",
- "fieldtype": "Check",
- "label": "Is Tax Applicable"
- },
- {
- "default": "1",
- "fieldname": "is_payable",
- "fieldtype": "Check",
- "label": "Is Payable"
- },
- {
- "default": "1",
- "fieldname": "depends_on_payment_days",
- "fieldtype": "Check",
- "label": "Depends on Payment Days",
- "print_hide": 1
- },
- {
- "default": "0",
- "fieldname": "do_not_include_in_total",
- "fieldtype": "Check",
- "label": "Do Not Include in Total"
- },
- {
- "default": "0",
- "depends_on": "is_tax_applicable",
- "fieldname": "deduct_full_tax_on_selected_payroll_date",
- "fieldtype": "Check",
- "label": "Deduct Full Tax on Selected Payroll Date"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "disabled",
- "fieldtype": "Check",
- "label": "Disabled"
- },
- {
- "fieldname": "description",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Description"
- },
- {
- "default": "0",
- "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ",
- "fieldname": "statistical_component",
- "fieldtype": "Check",
- "label": "Statistical Component"
- },
- {
- "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1",
- "fieldname": "flexible_benefits",
- "fieldtype": "Section Break",
- "label": "Flexible Benefits"
- },
- {
- "default": "0",
- "fieldname": "is_flexible_benefit",
- "fieldtype": "Check",
- "label": "Is Flexible Benefit"
- },
- {
- "depends_on": "is_flexible_benefit",
- "fieldname": "max_benefit_amount",
- "fieldtype": "Currency",
- "label": "Max Benefit Amount (Yearly)"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "depends_on": "is_flexible_benefit",
- "fieldname": "pay_against_benefit_claim",
- "fieldtype": "Check",
- "label": "Pay Against Benefit Claim"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1",
- "fieldname": "only_tax_impact",
- "fieldtype": "Check",
- "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1",
- "fieldname": "create_separate_payment_entry_against_benefit_claim",
- "fieldtype": "Check",
- "label": "Create Separate Payment Entry Against Benefit Claim"
- },
- {
- "depends_on": "eval:doc.type=='Deduction'",
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "fieldname": "variable_based_on_taxable_salary",
- "fieldtype": "Check",
- "label": "Variable Based On Taxable Salary"
- },
- {
- "depends_on": "eval:doc.statistical_component != 1",
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "label": "Accounts"
- },
- {
- "fieldname": "accounts",
- "fieldtype": "Table",
- "label": "Accounts",
- "options": "Salary Component Account"
- },
- {
- "collapsible": 1,
- "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1",
- "fieldname": "condition_and_formula",
- "fieldtype": "Section Break",
- "label": "Condition and Formula"
- },
- {
- "fieldname": "condition",
- "fieldtype": "Code",
- "label": "Condition"
- },
- {
- "default": "0",
- "fieldname": "amount_based_on_formula",
- "fieldtype": "Check",
- "label": "Amount based on formula"
- },
- {
- "depends_on": "amount_based_on_formula",
- "fieldname": "formula",
- "fieldtype": "Code",
- "label": "Formula"
- },
- {
- "depends_on": "eval:doc.amount_based_on_formula!==1",
- "fieldname": "amount",
- "fieldtype": "Currency",
- "label": "Amount"
- },
- {
- "fieldname": "column_break_28",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "help",
- "fieldtype": "HTML",
- "label": "Help",
- "options": "Help
\n\n
Notes:
\n\n\n- Use field
base for using base salary of the Employee \n- Use Salary Component abbreviations in conditions and formulas.
BS = Basic Salary \n- Use field name for employee details in conditions and formulas.
Employment Type = employment_typeBranch = branch \n- Use field name from Salary Slip in conditions and formulas.
Payment Days = payment_daysLeave without pay = leave_without_pay \n- Direct Amount can also be entered based on Condtion. See example 3
\n\nExamples
\n\n- Calculating Basic Salary based on
base\nCondition: base < 10000
\nFormula: base * .2
\n- Calculating HRA based on Basic Salary
BS \nCondition: BS > 2000
\nFormula: BS * .1
\n- Calculating TDS based on Employment Type
employment_type \nCondition: employment_type==\"Intern\"
\nAmount: 1000
\n
"
- },
- {
- "default": "0",
- "fieldname": "round_to_the_nearest_integer",
- "fieldtype": "Check",
- "label": "Round to the Nearest Integer"
- }
- ],
- "icon": "fa fa-flag",
- "modified": "2019-06-05 11:34:14.231228",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Salary Component",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "share": 1,
- "write": 1
- },
- {
- "read": 1,
- "role": "Employee"
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC"
- }
\ No newline at end of file
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:salary_component",
+ "creation": "2016-06-30 15:42:43.631931",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "salary_component",
+ "salary_component_abbr",
+ "type",
+ "description",
+ "column_break_4",
+ "depends_on_payment_days",
+ "is_tax_applicable",
+ "deduct_full_tax_on_selected_payroll_date",
+ "variable_based_on_taxable_salary",
+ "exempted_from_income_tax",
+ "round_to_the_nearest_integer",
+ "statistical_component",
+ "do_not_include_in_total",
+ "disabled",
+ "flexible_benefits",
+ "is_flexible_benefit",
+ "max_benefit_amount",
+ "column_break_9",
+ "pay_against_benefit_claim",
+ "only_tax_impact",
+ "create_separate_payment_entry_against_benefit_claim",
+ "section_break_5",
+ "accounts",
+ "condition_and_formula",
+ "condition",
+ "amount",
+ "amount_based_on_formula",
+ "formula",
+ "column_break_28",
+ "help"
+ ],
+ "fields": [
+ {
+ "fieldname": "salary_component",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "salary_component_abbr",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Abbr",
+ "print_width": "120px",
+ "reqd": 1,
+ "width": "120px"
+ },
+ {
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Type",
+ "options": "Earning\nDeduction",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:doc.type == \"Earning\"",
+ "fieldname": "is_tax_applicable",
+ "fieldtype": "Check",
+ "label": "Is Tax Applicable"
+ },
+ {
+ "default": "1",
+ "fieldname": "depends_on_payment_days",
+ "fieldtype": "Check",
+ "label": "Depends on Payment Days",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "do_not_include_in_total",
+ "fieldtype": "Check",
+ "label": "Do Not Include in Total"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.is_tax_applicable && doc.type=='Earning'",
+ "fieldname": "deduct_full_tax_on_selected_payroll_date",
+ "fieldtype": "Check",
+ "label": "Deduct Full Tax on Selected Payroll Date"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ },
+ {
+ "default": "0",
+ "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ",
+ "fieldname": "statistical_component",
+ "fieldtype": "Check",
+ "label": "Statistical Component"
+ },
+ {
+ "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1",
+ "fieldname": "flexible_benefits",
+ "fieldtype": "Section Break",
+ "label": "Flexible Benefits"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_flexible_benefit",
+ "fieldtype": "Check",
+ "label": "Is Flexible Benefit"
+ },
+ {
+ "depends_on": "is_flexible_benefit",
+ "fieldname": "max_benefit_amount",
+ "fieldtype": "Currency",
+ "label": "Max Benefit Amount (Yearly)"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "is_flexible_benefit",
+ "fieldname": "pay_against_benefit_claim",
+ "fieldtype": "Check",
+ "label": "Pay Against Benefit Claim"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1",
+ "fieldname": "only_tax_impact",
+ "fieldtype": "Check",
+ "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1",
+ "fieldname": "create_separate_payment_entry_against_benefit_claim",
+ "fieldtype": "Check",
+ "label": "Create Separate Payment Entry Against Benefit Claim"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.type == \"Deduction\"",
+ "fieldname": "variable_based_on_taxable_salary",
+ "fieldtype": "Check",
+ "label": "Variable Based On Taxable Salary"
+ },
+ {
+ "depends_on": "eval:doc.statistical_component != 1",
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Accounts"
+ },
+ {
+ "fieldname": "accounts",
+ "fieldtype": "Table",
+ "label": "Accounts",
+ "options": "Salary Component Account"
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1",
+ "fieldname": "condition_and_formula",
+ "fieldtype": "Section Break",
+ "label": "Condition and Formula"
+ },
+ {
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Condition"
+ },
+ {
+ "default": "0",
+ "fieldname": "amount_based_on_formula",
+ "fieldtype": "Check",
+ "label": "Amount based on formula"
+ },
+ {
+ "depends_on": "amount_based_on_formula",
+ "fieldname": "formula",
+ "fieldtype": "Code",
+ "label": "Formula"
+ },
+ {
+ "depends_on": "eval:doc.amount_based_on_formula!==1",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount"
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "help",
+ "fieldtype": "HTML",
+ "label": "Help",
+ "options": "Help
\n\nNotes:
\n\n\n- Use field
base for using base salary of the Employee \n- Use Salary Component abbreviations in conditions and formulas.
BS = Basic Salary \n- Use field name for employee details in conditions and formulas.
Employment Type = employment_typeBranch = branch \n- Use field name from Salary Slip in conditions and formulas.
Payment Days = payment_daysLeave without pay = leave_without_pay \n- Direct Amount can also be entered based on Condtion. See example 3
\n\nExamples
\n\n- Calculating Basic Salary based on
base\nCondition: base < 10000
\nFormula: base * .2
\n- Calculating HRA based on Basic Salary
BS \nCondition: BS > 2000
\nFormula: BS * .1
\n- Calculating TDS based on Employment Type
employment_type \nCondition: employment_type==\"Intern\"
\nAmount: 1000
\n
"
+ },
+ {
+ "default": "0",
+ "fieldname": "round_to_the_nearest_integer",
+ "fieldtype": "Check",
+ "label": "Round to the Nearest Integer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.type == \"Deduction\" && !doc.variable_based_on_taxable_salary",
+ "description": "If checked, the full amount will be deducted from taxable income before calculating income tax without any declaration or proof submission.",
+ "fieldname": "exempted_from_income_tax",
+ "fieldtype": "Check",
+ "label": "Exempted from Income Tax"
+ }
+ ],
+ "icon": "fa fa-flag",
+ "links": [],
+ "modified": "2020-04-28 15:46:45.252945",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Salary Component",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "Employee"
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/salary_component/test_records.json b/erpnext/hr/doctype/salary_component/test_records.json
index 7b22b481f36..104b44ffa1b 100644
--- a/erpnext/hr/doctype/salary_component/test_records.json
+++ b/erpnext/hr/doctype/salary_component/test_records.json
@@ -3,14 +3,12 @@
"doctype": "Salary Component",
"salary_component": "_Test Basic Salary",
"type": "Earning",
- "is_payable": 1,
"is_tax_applicable": 1
},
{
"doctype": "Salary Component",
"salary_component": "_Test Allowance",
"type": "Earning",
- "is_payable": 1,
"is_tax_applicable": 1
},
{
@@ -27,14 +25,12 @@
"doctype": "Salary Component",
"salary_component": "Basic",
"type": "Earning",
- "is_payable": 1,
"is_tax_applicable": 1
},
{
"doctype": "Salary Component",
"salary_component": "Leave Encashment",
"type": "Earning",
- "is_payable": 1,
"is_tax_applicable": 1
}
]
\ No newline at end of file
diff --git a/erpnext/hr/doctype/salary_component/test_salary_component.py b/erpnext/hr/doctype/salary_component/test_salary_component.py
index 965cc9e9ffd..4f7db0c71ca 100644
--- a/erpnext/hr/doctype/salary_component/test_salary_component.py
+++ b/erpnext/hr/doctype/salary_component/test_salary_component.py
@@ -18,6 +18,5 @@ def create_salary_component(component_name, **args):
"doctype": "Salary Component",
"salary_component": component_name,
"type": args.get("type") or "Earning",
- "is_payable": args.get("is_payable") or 1,
"is_tax_applicable": args.get("is_tax_applicable") or 1
}).insert()
diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json
index edf2786821e..545f56a0b60 100644
--- a/erpnext/hr/doctype/salary_detail/salary_detail.json
+++ b/erpnext/hr/doctype/salary_detail/salary_detail.json
@@ -1,765 +1,216 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-06-30 15:32:36.385111",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-06-30 15:32:36.385111",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "salary_component",
+ "abbr",
+ "statistical_component",
+ "column_break_3",
+ "deduct_full_tax_on_selected_payroll_date",
+ "depends_on_payment_days",
+ "is_tax_applicable",
+ "exempted_from_income_tax",
+ "is_flexible_benefit",
+ "variable_based_on_taxable_salary",
+ "section_break_2",
+ "condition",
+ "amount_based_on_formula",
+ "formula",
+ "amount",
+ "do_not_include_in_total",
+ "default_amount",
+ "additional_amount",
+ "tax_on_flexible_benefit",
+ "tax_on_additional_salary",
+ "section_break_11",
+ "condition_and_formula_help"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "salary_component",
- "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": "Component",
- "length": 0,
- "no_copy": 0,
- "options": "Salary Component",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "salary_component",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Component",
+ "options": "Salary Component",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "default": "",
- "depends_on": "eval:doc.parenttype=='Salary Structure'",
- "fetch_from": "salary_component.salary_component_abbr",
- "fetch_if_empty": 0,
- "fieldname": "abbr",
- "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": "Abbr",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 1,
+ "depends_on": "eval:doc.parenttype=='Salary Structure'",
+ "fetch_from": "salary_component.salary_component_abbr",
+ "fieldname": "abbr",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Abbr",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 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,
- "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ",
- "fetch_from": "salary_component.statistical_component",
- "fetch_if_empty": 0,
- "fieldname": "statistical_component",
- "fieldtype": "Check",
- "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": "Statistical Component",
- "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",
+ "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ",
+ "fetch_from": "salary_component.statistical_component",
+ "fieldname": "statistical_component",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Statistical Component"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "salary_component.is_tax_applicable",
- "fetch_if_empty": 0,
- "fieldname": "is_tax_applicable",
- "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": "Is Tax Applicable",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:doc.parentfield=='earnings'",
+ "fetch_from": "salary_component.is_tax_applicable",
+ "fieldname": "is_tax_applicable",
+ "fieldtype": "Check",
+ "label": "Is Tax Applicable",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "salary_component.is_flexible_benefit",
- "fetch_if_empty": 0,
- "fieldname": "is_flexible_benefit",
- "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": "Is Flexible Benefit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:doc.parentfield=='earnings'",
+ "fetch_from": "salary_component.is_flexible_benefit",
+ "fieldname": "is_flexible_benefit",
+ "fieldtype": "Check",
+ "label": "Is Flexible Benefit",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fetch_from": "salary_component.variable_based_on_taxable_salary",
- "fetch_if_empty": 0,
- "fieldname": "variable_based_on_taxable_salary",
- "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": "Variable Based On Taxable Salary",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:doc.parentfield=='deductions'",
+ "fetch_from": "salary_component.variable_based_on_taxable_salary",
+ "fieldname": "variable_based_on_taxable_salary",
+ "fieldtype": "Check",
+ "label": "Variable Based On Taxable Salary",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_from": "salary_component.depends_on_payment_days",
- "fetch_if_empty": 0,
- "fieldname": "depends_on_payment_days",
- "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": "Depends on Payment Days",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fetch_from": "salary_component.depends_on_payment_days",
+ "fieldname": "depends_on_payment_days",
+ "fieldtype": "Check",
+ "label": "Depends on Payment Days",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "deduct_full_tax_on_selected_payroll_date",
- "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": "Deduct Full Tax on Selected Payroll Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "deduct_full_tax_on_selected_payroll_date",
+ "fieldtype": "Check",
+ "label": "Deduct Full Tax on Selected Payroll Date",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.is_flexible_benefit != 1",
- "fetch_if_empty": 0,
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.is_flexible_benefit != 1",
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.parenttype=='Salary Structure'",
- "fetch_if_empty": 0,
- "fieldname": "condition",
- "fieldtype": "Code",
- "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": "Condition",
- "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
- },
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.parenttype=='Salary Structure'",
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Condition"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "eval:doc.parenttype=='Salary Structure'",
- "fetch_from": "",
- "fetch_if_empty": 0,
- "fieldname": "amount_based_on_formula",
- "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": "Amount based on formula",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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",
+ "depends_on": "eval:doc.parenttype=='Salary Structure'",
+ "fieldname": "amount_based_on_formula",
+ "fieldtype": "Check",
+ "label": "Amount based on formula"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "depends_on": "eval:doc.amount_based_on_formula!==0 && doc.parenttype==='Salary Structure'",
- "description": "",
- "fetch_if_empty": 0,
- "fieldname": "formula",
- "fieldtype": "Code",
- "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": "Formula",
- "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
- },
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.amount_based_on_formula!==0 && doc.parenttype==='Salary Structure'",
+ "fieldname": "formula",
+ "fieldtype": "Code",
+ "in_list_view": 1,
+ "label": "Formula"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.amount_based_on_formula!==1 || doc.parenttype==='Salary Slip'",
- "fetch_if_empty": 0,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.amount_based_on_formula!==1 || doc.parenttype==='Salary Slip'",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "Company:company:default_currency"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "do_not_include_in_total",
- "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": "Do not include in total",
- "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": "do_not_include_in_total",
+ "fieldtype": "Check",
+ "label": "Do not include in total"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.parenttype=='Salary Structure'",
- "fetch_if_empty": 0,
- "fieldname": "default_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Default Amount",
- "length": 0,
- "no_copy": 0,
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.parenttype=='Salary Structure'",
+ "fieldname": "default_amount",
+ "fieldtype": "Currency",
+ "label": "Default Amount",
+ "options": "Company:company:default_currency",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fetch_from": "",
- "fetch_if_empty": 0,
- "fieldname": "additional_amount",
- "fieldtype": "Currency",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Additional Amount",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "additional_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Additional Amount",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1",
- "fetch_if_empty": 0,
- "fieldname": "tax_on_flexible_benefit",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Tax on flexible benefit",
- "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
- },
+ "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1",
+ "fieldname": "tax_on_flexible_benefit",
+ "fieldtype": "Currency",
+ "label": "Tax on flexible benefit",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1",
- "fetch_if_empty": 0,
- "fieldname": "tax_on_additional_salary",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Tax on additional salary",
- "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
- },
+ "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1",
+ "fieldname": "tax_on_additional_salary",
+ "fieldtype": "Currency",
+ "label": "Tax on additional salary",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.parenttype=='Salary Structure'",
- "fetch_if_empty": 0,
- "fieldname": "section_break_11",
- "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
- },
+ "depends_on": "eval:doc.parenttype=='Salary Structure'",
+ "fieldname": "section_break_11",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.parenttype=='Salary Structure'",
- "fetch_if_empty": 0,
- "fieldname": "condition_and_formula_help",
- "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": "Condition and Formula Help",
- "length": 0,
- "no_copy": 0,
- "options": "Condition and Formula Help
\n\nNotes:
\n\n\n- Use field
base for using base salary of the Employee \n- Use Salary Component abbreviations in conditions and formulas.
BS = Basic Salary \n- Use field name for employee details in conditions and formulas.
Employment Type = employment_typeBranch = branch \n- Use field name from Salary Slip in conditions and formulas.
Payment Days = payment_daysLeave without pay = leave_without_pay \n- Direct Amount can also be entered based on Condtion. See example 3
\n\nExamples
\n\n- Calculating Basic Salary based on
base\nCondition: base < 10000
\nFormula: base * .2
\n- Calculating HRA based on Basic Salary
BS \nCondition: BS > 2000
\nFormula: BS * .1
\n- Calculating TDS based on Employment Type
employment_type \nCondition: employment_type==\"Intern\"
\nAmount: 1000
\n
",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "depends_on": "eval:doc.parenttype=='Salary Structure'",
+ "fieldname": "condition_and_formula_help",
+ "fieldtype": "HTML",
+ "label": "Condition and Formula Help",
+ "options": "Condition and Formula Help
\n\nNotes:
\n\n\n- Use field
base for using base salary of the Employee \n- Use Salary Component abbreviations in conditions and formulas.
BS = Basic Salary \n- Use field name for employee details in conditions and formulas.
Employment Type = employment_typeBranch = branch \n- Use field name from Salary Slip in conditions and formulas.
Payment Days = payment_daysLeave without pay = leave_without_pay \n- Direct Amount can also be entered based on Condtion. See example 3
\n\nExamples
\n\n- Calculating Basic Salary based on
base\nCondition: base < 10000
\nFormula: base * .2
\n- Calculating HRA based on Basic Salary
BS \nCondition: BS > 2000
\nFormula: BS * .1
\n- Calculating TDS based on Employment Type
employment_type \nCondition: employment_type==\"Intern\"
\nAmount: 1000
\n
"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.parentfield=='deductions'",
+ "fetch_from": "salary_component.exempted_from_income_tax",
+ "fieldname": "exempted_from_income_tax",
+ "fieldtype": "Check",
+ "label": "Exempted from Income Tax",
+ "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": "2019-05-11 17:33:08.508653",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Salary Detail",
- "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": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-04-24 20:00:16.475295",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Salary Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py
index 6157daff1e0..452aa74281e 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.py
@@ -450,7 +450,8 @@ class SalarySlip(TransactionBase):
'is_flexible_benefit': struct_row.is_flexible_benefit,
'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary,
'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date,
- 'additional_amount': amount if struct_row.get("is_additional_component") else 0
+ 'additional_amount': amount if struct_row.get("is_additional_component") else 0,
+ 'exempted_from_income_tax': struct_row.exempted_from_income_tax
})
else:
if struct_row.get("is_additional_component"):
@@ -481,20 +482,23 @@ class SalarySlip(TransactionBase):
return self.calculate_variable_tax(payroll_period, tax_component)
def calculate_variable_tax(self, payroll_period, tax_component):
+ # get Tax slab from salary structure assignment for the employee and payroll period
+ tax_slab = self.get_income_tax_slabs(payroll_period)
+
# get remaining numbers of sub-period (period for which one salary is processed)
remaining_sub_periods = get_period_factor(self.employee,
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
-
# get taxable_earnings, paid_taxes for previous period
- previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date)
+ previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
+ self.start_date, tax_slab.allow_tax_exemption)
previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component)
# get taxable_earnings for current period (all days)
- current_taxable_earnings = self.get_taxable_earnings()
+ current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption)
future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1)
# get taxable_earnings, addition_earnings for current actual payment days
- current_taxable_earnings_for_payment_days = self.get_taxable_earnings(based_on_payment_days=1)
+ current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1)
current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings
current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income
current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax
@@ -506,23 +510,27 @@ class SalarySlip(TransactionBase):
unclaimed_taxable_benefits += current_taxable_earnings_for_payment_days.flexi_benefits
# Total exemption amount based on tax exemption declaration
- total_exemption_amount, other_incomes = self.get_total_exemption_amount_and_other_incomes(payroll_period)
+ total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab)
+
+ #Employee Other Incomes
+ other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0
# Total taxable earnings including additional and other incomes
total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \
+ current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount
-
+
# Total taxable earnings without additional earnings with full tax
total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax
# Structured tax amount
- total_structured_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings_without_full_tax_addl_components)
+ total_structured_tax_amount = self.calculate_tax_by_tax_slab(
+ total_taxable_earnings_without_full_tax_addl_components, tax_slab)
current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods
-
+
# Total taxable earnings with additional earnings with full tax
full_tax_on_additional_earnings = 0.0
if current_additional_earnings_with_full_tax:
- total_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings)
+ total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab)
full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount
current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings
@@ -531,12 +539,30 @@ class SalarySlip(TransactionBase):
return current_tax_amount
- def get_taxable_earnings_for_prev_period(self, start_date, end_date):
+ def get_income_tax_slabs(self, payroll_period):
+ income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment",
+ {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name'])
+
+ if not income_tax_slab:
+ frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name))
+
+ income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab)
+ if income_tax_slab_doc.disabled:
+ frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab))
+
+ if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date):
+ frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}")
+ .format(payroll_period.start_date))
+
+ return income_tax_slab_doc
+
+
+ def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_exemption=False):
taxable_earnings = frappe.db.sql("""
select sum(sd.amount)
from
`tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
- where
+ where
sd.parentfield='earnings'
and sd.is_tax_applicable=1
and is_flexible_benefit=0
@@ -549,7 +575,30 @@ class SalarySlip(TransactionBase):
"from_date": start_date,
"to_date": end_date
})
- return flt(taxable_earnings[0][0]) if taxable_earnings else 0
+ taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0
+
+ exempted_amount = 0
+ if allow_tax_exemption:
+ exempted_amount = frappe.db.sql("""
+ select sum(sd.amount)
+ from
+ `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
+ where
+ sd.parentfield='deductions'
+ and sd.exempted_from_income_tax=1
+ and is_flexible_benefit=0
+ and ss.docstatus=1
+ and ss.employee=%(employee)s
+ and ss.start_date between %(from_date)s and %(to_date)s
+ and ss.end_date between %(from_date)s and %(to_date)s
+ """, {
+ "employee": self.employee,
+ "from_date": start_date,
+ "to_date": end_date
+ })
+ exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0
+
+ return taxable_earnings - exempted_amount
def get_tax_paid_in_period(self, start_date, end_date, tax_component):
# find total_tax_paid, tax paid for benefit, additional_salary
@@ -575,7 +624,7 @@ class SalarySlip(TransactionBase):
return total_tax_paid
- def get_taxable_earnings(self, based_on_payment_days=0):
+ def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
@@ -609,6 +658,14 @@ class SalarySlip(TransactionBase):
else:
taxable_earnings += amount
+ if allow_tax_exemption:
+ for ded in self.deductions:
+ if ded.exempted_from_income_tax:
+ amount = ded.amount
+ if based_on_payment_days:
+ amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0]
+ taxable_earnings -= flt(amount)
+
return frappe._dict({
"taxable_earnings": taxable_earnings,
"additional_income": additional_income,
@@ -671,40 +728,63 @@ class SalarySlip(TransactionBase):
return total_benefits_paid - total_benefits_claimed
- def get_total_exemption_amount_and_other_incomes(self, payroll_period):
- total_exemption_amount, other_incomes = 0, 0
- if self.deduct_tax_for_unsubmitted_tax_exemption_proof:
- exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission",
- {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1},
- ["exemption_amount", "income_from_other_sources"])
- if exemption_proof:
- total_exemption_amount, other_incomes = exemption_proof
- else:
- declaration = frappe.db.get_value("Employee Tax Exemption Declaration",
- {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1},
- ["total_exemption_amount", "income_from_other_sources"])
- if declaration:
- total_exemption_amount, other_incomes = declaration
+ def get_total_exemption_amount(self, payroll_period, tax_slab):
+ total_exemption_amount = 0
+ if tax_slab.allow_tax_exemption:
+ if self.deduct_tax_for_unsubmitted_tax_exemption_proof:
+ exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission",
+ {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1},
+ ["exemption_amount"])
+ if exemption_proof:
+ total_exemption_amount = exemption_proof
+ else:
+ declaration = frappe.db.get_value("Employee Tax Exemption Declaration",
+ {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1},
+ ["total_exemption_amount"])
+ if declaration:
+ total_exemption_amount = declaration
- return total_exemption_amount, other_incomes
+ total_exemption_amount += flt(tax_slab.standard_tax_exemption_amount)
- def calculate_tax_by_tax_slab(self, payroll_period, annual_taxable_earning):
- payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period)
- annual_taxable_earning -= flt(payroll_period_obj.standard_tax_exemption_amount)
+ return total_exemption_amount
+
+ def get_income_form_other_sources(self, payroll_period):
+ return frappe.get_all("Employee Other Income",
+ filters={
+ "employee": self.employee,
+ "payroll_period": payroll_period.name,
+ "company": self.company,
+ "docstatus": 1
+ },
+ fields="SUM(amount) as total_amount"
+ )[0].total_amount
+
+ def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab):
data = self.get_data_for_eval()
data.update({"annual_taxable_earning": annual_taxable_earning})
- taxable_amount = 0
- for slab in payroll_period_obj.taxable_salary_slabs:
+ tax_amount = 0
+ for slab in tax_slab.slabs:
if slab.condition and not self.eval_tax_slab_condition(slab.condition, data):
continue
- if not slab.to_amount and annual_taxable_earning > slab.from_amount:
- taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01
+ if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
+ tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
continue
- if annual_taxable_earning > slab.from_amount and annual_taxable_earning < slab.to_amount:
- taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01
- elif annual_taxable_earning > slab.from_amount and annual_taxable_earning > slab.to_amount:
- taxable_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01
- return taxable_amount
+ if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount:
+ tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
+ elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount:
+ tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * .01
+
+ # other taxes and charges on income tax
+ for d in tax_slab.other_taxes_and_charges:
+ if flt(d.min_taxable_income) and flt(d.min_taxable_income) > tax_amount:
+ continue
+
+ if flt(d.max_taxable_income) and flt(d.max_taxable_income) < tax_amount:
+ continue
+
+ tax_amount += tax_amount * flt(d.percent) / 100
+
+ return tax_amount
def eval_tax_slab_condition(self, condition, data):
try:
diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py
index 16a75f473f4..7572e00359a 100644
--- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py
@@ -47,10 +47,7 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.payment_days, no_of_days[0])
self.assertEqual(ss.earnings[0].amount, 50000)
self.assertEqual(ss.earnings[1].amount, 3000)
- self.assertEqual(ss.deductions[0].amount, 5000)
- self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
- self.assertEqual(ss.net_pay, 67418.0)
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
@@ -67,10 +64,7 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(ss.earnings[0].amount, 50000)
self.assertEqual(ss.earnings[0].default_amount, 50000)
self.assertEqual(ss.earnings[1].amount, 3000)
- self.assertEqual(ss.deductions[0].amount, 5000)
- self.assertEqual(ss.deductions[1].amount, 5000)
self.assertEqual(ss.gross_pay, 78000)
- self.assertEqual(ss.net_pay, 67418.0)
def test_payment_days(self):
no_of_days = self.get_no_of_days()
@@ -80,8 +74,8 @@ class TestSalarySlip(unittest.TestCase):
# set joinng date in the same month
make_employee("test_employee@salary.com")
if getdate(nowdate()).day >= 15:
- date_of_joining = getdate(add_days(nowdate(),-10))
relieving_date = getdate(add_days(nowdate(),-10))
+ date_of_joining = getdate(add_days(nowdate(),-10))
elif getdate(nowdate()).day < 15 and getdate(nowdate()).day >= 5:
date_of_joining = getdate(add_days(nowdate(),-3))
relieving_date = getdate(add_days(nowdate(),-3))
@@ -131,9 +125,7 @@ class TestSalarySlip(unittest.TestCase):
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
- hr_settings = frappe.get_doc("HR Settings", "HR Settings")
- hr_settings.email_salary_slip_to_employee = 1
- hr_settings.save()
+ frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 1)
make_employee("test_employee@salary.com")
ss = make_employee_salary_slip("test_employee@salary.com", "Monthly")
@@ -183,8 +175,11 @@ class TestSalarySlip(unittest.TestCase):
# as per assigned salary structure 40500 in monthly salary so 236000*5/100/12
frappe.db.sql("""delete from `tabPayroll Period`""")
frappe.db.sql("""delete from `tabSalary Component`""")
+
payroll_period = create_payroll_period()
- create_tax_slab(payroll_period)
+
+ create_tax_slab(payroll_period, allow_tax_exemption=True)
+
employee = make_employee("test_tax@salary.slip")
delete_docs = [
"Salary Slip",
@@ -210,8 +205,7 @@ class TestSalarySlip(unittest.TestCase):
payroll_period, deduct_random=False)
tax_paid = get_tax_paid_in_period(employee)
- # total taxable income 586000, 250000 @ 5%, 86000 @ 20% ie. 12500 + 17200
- annual_tax = 113568
+ annual_tax = 113589.0
try:
self.assertEqual(tax_paid, annual_tax)
except AssertionError:
@@ -235,8 +229,7 @@ class TestSalarySlip(unittest.TestCase):
raise
# Submit proof for total 120000
- data["proof-1"] = create_proof_submission(employee, payroll_period, 50000)
- data["proof-2"] = create_proof_submission(employee, payroll_period, 70000)
+ data["proof"] = create_proof_submission(employee, payroll_period, 120000)
# Submit benefit claim for total 50000
data["benefit-1"] = create_benefit_claim(employee, payroll_period, 15000, "Medical Allowance")
@@ -250,7 +243,7 @@ class TestSalarySlip(unittest.TestCase):
# total taxable income 416000, 166000 @ 5% ie. 8300
try:
- self.assertEqual(tax_paid, 88608)
+ self.assertEqual(tax_paid, 82389.0)
except AssertionError:
print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n")
raise
@@ -265,7 +258,7 @@ class TestSalarySlip(unittest.TestCase):
# total taxable income 566000, 250000 @ 5%, 66000 @ 20%, 12500 + 13200
tax_paid = get_tax_paid_in_period(employee)
try:
- self.assertEqual(tax_paid, 121211)
+ self.assertEqual(tax_paid, annual_tax)
except AssertionError:
print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n")
raise
@@ -307,6 +300,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
+
employee = frappe.db.get_value("Employee", {"user_id": user})
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee)
salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
@@ -431,17 +425,15 @@ def make_deduction_salary_component(setup=False, test_tax=False):
{
"salary_component": 'Professional Tax',
"abbr":'PT',
- "condition": 'base > 10000',
- "formula": 'base*.1',
"type": "Deduction",
- "amount_based_on_formula": 1
+ "amount": 200,
+ "exempted_from_income_tax": 1
+
},
{
"salary_component": 'TDS',
"abbr":'T',
- "formula": 'base*.1',
"type": "Deduction",
- "amount_based_on_formula": 1,
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
@@ -452,9 +444,7 @@ def make_deduction_salary_component(setup=False, test_tax=False):
"salary_component": 'TDS',
"abbr":'T',
"condition": 'employment_type=="Intern"',
- "formula": 'base*.1',
"type": "Deduction",
- "amount_based_on_formula": 1,
"round_to_the_nearest_integer": 1
})
if setup or test_tax:
@@ -510,29 +500,47 @@ def create_benefit_claim(employee, payroll_period, amount, component):
}).submit()
return claim_date
-def create_tax_slab(payroll_period):
- data = [
+def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False):
+ if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name):
+ return
+
+ slabs = [
{
"from_amount": 250000,
"to_amount": 500000,
- "percent_deduction": 5.2,
+ "percent_deduction": 5,
"condition": "annual_taxable_earning > 500000"
},
{
"from_amount": 500001,
"to_amount": 1000000,
- "percent_deduction": 20.8
+ "percent_deduction": 20
},
{
"from_amount": 1000001,
- "percent_deduction": 31.2
+ "percent_deduction": 30
}
]
- payroll_period.taxable_salary_slabs = []
- for item in data:
- payroll_period.append("taxable_salary_slabs", item)
- payroll_period.standard_tax_exemption_amount = 52500
- payroll_period.save()
+
+ income_tax_slab = frappe.new_doc("Income Tax Slab")
+ income_tax_slab.name = "Tax Slab: " + payroll_period.name
+ income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
+
+ if allow_tax_exemption:
+ income_tax_slab.allow_tax_exemption = 1
+ income_tax_slab.standard_tax_exemption_amount = 50000
+
+ for item in slabs:
+ income_tax_slab.append("slabs", item)
+
+ income_tax_slab.append("other_taxes_and_charges", {
+ "description": "cess",
+ "percent": 4
+ })
+
+ income_tax_slab.save()
+ if not dont_submit:
+ income_tax_slab.submit()
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
deducted_dates = []
diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.js b/erpnext/hr/doctype/salary_structure/salary_structure.js
index 672be0c2fc2..2f2c815d70c 100755
--- a/erpnext/hr/doctype/salary_structure/salary_structure.js
+++ b/erpnext/hr/doctype/salary_structure/salary_structure.js
@@ -82,6 +82,7 @@ frappe.ui.form.on('Salary Structure', {
{fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")},
{fieldname:'base_variable', fieldtype:'Section Break'},
{fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1},
+ {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'},
{fieldname:'base_col_br', fieldtype:'Column Break'},
{fieldname:'base', fieldtype:'Currency', label: __('Base')},
{fieldname:'variable', fieldtype:'Currency', label: __('Variable')}
diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py
index 568277f8a73..5ba7f1c4327 100644
--- a/erpnext/hr/doctype/salary_structure/salary_structure.py
+++ b/erpnext/hr/doctype/salary_structure/salary_structure.py
@@ -16,6 +16,7 @@ class SalaryStructure(Document):
self.validate_amount()
self.strip_condition_and_formula_fields()
self.validate_max_benefits_with_flexi()
+ self.validate_component_based_on_tax_slab()
def set_missing_values(self):
overwritten_fields = ["depends_on_payment_days", "variable_based_on_taxable_salary", "is_tax_applicable", "is_flexible_benefit"]
@@ -34,6 +35,12 @@ class SalaryStructure(Document):
for fieldname in overwritten_fields_if_missing:
d.set(fieldname, component_default_value.get(fieldname))
+ def validate_component_based_on_tax_slab(self):
+ for row in self.deductions:
+ if row.variable_based_on_taxable_salary and (row.amount or row.formula):
+ frappe.throw(_("Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary")
+ .format(row.idx, row.salary_component))
+
def validate_amount(self):
if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet:
frappe.throw(_("Net pay cannot be negative"))
@@ -82,21 +89,23 @@ class SalaryStructure(Document):
@frappe.whitelist()
def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None,
- from_date=None, base=None,variable=None):
+ from_date=None, base=None, variable=None, income_tax_slab=None):
employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee)
if employees:
if len(employees) > 20:
frappe.enqueue(assign_salary_structure_for_employees, timeout=600,
- employees=employees, salary_structure=self,from_date=from_date, base=base,variable=variable)
+ employees=employees, salary_structure=self,from_date=from_date,
+ base=base, variable=variable, income_tax_slab=income_tax_slab)
else:
- assign_salary_structure_for_employees(employees, self, from_date=from_date, base=base,variable=variable)
+ assign_salary_structure_for_employees(employees, self, from_date=from_date,
+ base=base, variable=variable, income_tax_slab=income_tax_slab)
else:
frappe.msgprint(_("No Employee Found"))
-def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None,variable=None):
+def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None):
salary_structures_assignments = []
existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date)
count=0
@@ -105,7 +114,8 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date
continue
count +=1
- salary_structures_assignment = create_salary_structures_assignment(employee, salary_structure, from_date, base, variable)
+ salary_structures_assignment = create_salary_structures_assignment(employee,
+ salary_structure, from_date, base, variable, income_tax_slab)
salary_structures_assignments.append(salary_structures_assignment)
frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures..."))
@@ -113,7 +123,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date
frappe.msgprint(_("Structures have been assigned successfully"))
-def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable):
+def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None):
assignment = frappe.new_doc("Salary Structure Assignment")
assignment.employee = employee
assignment.salary_structure = salary_structure.name
@@ -121,6 +131,7 @@ def create_salary_structures_assignment(employee, salary_structure, from_date, b
assignment.from_date = from_date
assignment.base = base
assignment.variable = variable
+ assignment.income_tax_slab = income_tax_slab
assignment.save(ignore_permissions = True)
assignment.submit()
return assignment.name
@@ -138,7 +149,7 @@ def get_existing_assignments(employees, salary_structure, from_date):
return salary_structures_assignments
@frappe.whitelist()
-def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0):
+def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0, ignore_permissions=False):
def postprocess(source, target):
if employee:
employee_details = frappe.db.get_value("Employee", employee,
@@ -158,7 +169,7 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print =
"name": "salary_structure"
}
}
- }, target_doc, postprocess, ignore_child_tables=True)
+ }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions)
if cint(as_print):
doc.name = 'Preview for {0}'.format(employee)
diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py
index f67a189ee71..3e55a1a068d 100644
--- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py
@@ -9,8 +9,9 @@ from frappe.utils.make_random import get_random
from frappe.utils import nowdate, add_days, add_years, getdate, add_months
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.hr.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\
- make_deduction_salary_component, make_employee_salary_slip
+ make_deduction_salary_component, make_employee_salary_slip, create_tax_slab
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_payroll_period
test_dependencies = ["Fiscal Year"]
@@ -70,10 +71,8 @@ class TestSalaryStructure(unittest.TestCase):
self.assertEqual(sal_slip.get("earnings")[1].amount, 3000)
self.assertEqual(sal_slip.get("earnings")[2].amount, 25000)
self.assertEqual(sal_slip.get("gross_pay"), 78000)
- self.assertEqual(sal_slip.get("deductions")[0].amount, 5000)
- self.assertEqual(sal_slip.get("deductions")[1].amount, 5000)
- self.assertEqual(sal_slip.get("total_deduction"), 10000)
- self.assertEqual(sal_slip.get("net_pay"), 68000)
+ self.assertEqual(sal_slip.get("deductions")[0].amount, 200)
+ self.assertEqual(sal_slip.get("net_pay"), 78000 - sal_slip.get("total_deduction"))
def test_whitespaces_in_formula_conditions_fields(self):
salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", dont_submit=True)
@@ -111,6 +110,7 @@ class TestSalaryStructure(unittest.TestCase):
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
+
if not frappe.db.exists('Salary Structure', salary_structure):
details = {
"doctype": "Salary Structure",
@@ -123,7 +123,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
}
if other_details and isinstance(other_details, dict):
details.update(other_details)
- salary_structure_doc = frappe.get_doc(details).insert()
+ salary_structure_doc = frappe.get_doc(details)
+ salary_structure_doc.insert()
if not dont_submit:
salary_structure_doc.submit()
else:
@@ -138,13 +139,18 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
def create_salary_structure_assignment(employee, salary_structure, from_date=None):
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
+
+ payroll_period = create_payroll_period()
+ create_tax_slab(payroll_period, allow_tax_exemption=True)
+
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
salary_structure_assignment.employee = employee
salary_structure_assignment.base = 50000
salary_structure_assignment.variable = 5000
- salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1)
+ salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1)
salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.company = erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True)
+ salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period"
salary_structure_assignment.submit()
return salary_structure_assignment
diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js
index 56a05e04956..818e853154d 100644
--- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js
+++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js
@@ -20,6 +20,16 @@ frappe.ui.form.on('Salary Structure Assignment', {
}
}
});
+
+ frm.set_query("income_tax_slab", function() {
+ return {
+ filters: {
+ company: frm.doc.company,
+ docstatus: 1,
+ disabled: 0
+ }
+ }
+ });
},
employee: function(frm) {
if(frm.doc.employee){
diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json
index 136eb0f8053..0098aa8ec80 100644
--- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -10,11 +10,12 @@
"employee",
"employee_name",
"department",
- "designation",
+ "company",
"column_break_6",
+ "designation",
"salary_structure",
"from_date",
- "company",
+ "income_tax_slab",
"section_break_7",
"base",
"column_break_9",
@@ -113,11 +114,17 @@
"options": "Salary Structure Assignment",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "income_tax_slab",
+ "fieldtype": "Link",
+ "label": "Income Tax Slab",
+ "options": "Income Tax Slab"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2019-12-31 17:05:28.637510",
+ "modified": "2020-04-25 18:24:23.617088",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Structure Assignment",
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 35c8630e8e2..d20018eef34 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -8,9 +8,6 @@ from frappe.utils import flt
from erpnext.hr.doctype.leave_application.leave_application \
import get_leave_balance_on, get_leaves_for_period
-from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary \
- import get_department_leave_approver_map
-
def execute(filters=None):
leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc")
@@ -28,6 +25,8 @@ def get_columns(leave_types):
for leave_type in leave_types:
columns.append(_(leave_type) + " " + _("Opening") + ":Float:160")
+ columns.append(_(leave_type) + " " + _("Allocated") + ":Float:160")
+ columns.append(_(leave_type) + " " + _("Expired") + ":Float:160")
columns.append(_(leave_type) + " " + _("Taken") + ":Float:160")
columns.append(_(leave_type) + " " + _("Balance") + ":Float:160")
@@ -68,18 +67,97 @@ def get_data(filters, leave_types):
row = [employee.name, employee.employee_name, employee.department]
for leave_type in leave_types:
- # leaves taken
- leaves_taken = get_leaves_for_period(employee.name, leave_type,
- filters.from_date, filters.to_date) * -1
- # opening balance
- opening = get_leave_balance_on(employee.name, leave_type, filters.from_date)
- # closing balance
- closing = max(opening - leaves_taken, 0)
-
- row += [opening, leaves_taken, closing]
+ row += calculate_leaves_details(filters, leave_type, employee)
data.append(row)
+ return data
- return data
\ No newline at end of file
+def calculate_leaves_details(filters, leave_type, employee):
+ ledger_entries = get_leave_ledger_entries(filters.from_date, filters.to_date, employee.name, leave_type)
+
+ #Leaves Deducted consist of both expired and leaves taken
+ leaves_deducted = get_leaves_for_period(employee.name, leave_type,
+ filters.from_date, filters.to_date) * -1
+
+ # removing expired leaves
+ leaves_taken = leaves_deducted - remove_expired_leave(ledger_entries)
+
+ opening = get_leave_balance_on(employee.name, leave_type, filters.from_date)
+
+ new_allocation , expired_allocation = get_allocated_and_expired_leaves(ledger_entries, filters.from_date, filters.to_date)
+
+ #removing leaves taken from expired_allocation
+ expired_leaves = max(expired_allocation - leaves_taken, 0)
+
+ #Formula for calculating closing balance
+ closing = max(opening + new_allocation - (leaves_taken + expired_leaves), 0)
+
+ return [opening, new_allocation, expired_leaves, leaves_taken, closing]
+
+
+def remove_expired_leave(records):
+ expired_within_period = 0
+ for record in records:
+ if record.is_expired:
+ expired_within_period += record.leaves
+ return expired_within_period * -1
+
+
+def get_allocated_and_expired_leaves(records, from_date, to_date):
+
+ from frappe.utils import getdate
+
+ new_allocation = 0
+ expired_leaves = 0
+
+ for record in records:
+ if record.to_date <= getdate(to_date) and record.leaves>0:
+ expired_leaves += record.leaves
+
+ if record.from_date >= getdate(from_date) and record.leaves>0:
+ new_allocation += record.leaves
+
+ return new_allocation, expired_leaves
+
+def get_leave_ledger_entries(from_date, to_date, employee, leave_type):
+ records= frappe.db.sql("""
+ SELECT
+ employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type
+ is_carry_forward, is_expired
+ FROM `tabLeave Ledger Entry`
+ WHERE employee=%(employee)s AND leave_type=%(leave_type)s
+ AND docstatus=1
+ AND (from_date between %(from_date)s AND %(to_date)s
+ OR to_date between %(from_date)s AND %(to_date)s
+ OR (from_date < %(from_date)s AND to_date > %(to_date)s))
+ """, {
+ "from_date": from_date,
+ "to_date": to_date,
+ "employee": employee,
+ "leave_type": leave_type
+ }, as_dict=1)
+
+ return records
+
+def get_department_leave_approver_map(department=None):
+ conditions=''
+ if department:
+ conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
+
+ # get current department and all its child
+ department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
+
+ # retrieve approvers list from current department and from its subsequent child departments
+ approver_list = frappe.get_all('Department Approver', filters={
+ 'parentfield': 'leave_approvers',
+ 'parent': ('in', department_list)
+ }, fields=['parent', 'approver'], as_list=1)
+
+ approvers = {}
+
+ for k, v in approver_list:
+ approvers.setdefault(k, []).append(v)
+
+ return approvers
\ No newline at end of file
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
index 777de022387..53f8f41915e 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
@@ -6,6 +6,7 @@ import frappe
from frappe.utils import flt
from frappe import _
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on
+from erpnext.hr.report.employee_leave_balance.employee_leave_balance import calculate_leaves_details , get_department_leave_approver_map
def execute(filters=None):
if filters.to_date <= filters.from_date:
@@ -38,17 +39,27 @@ def get_columns():
'label': _('Opening Balance'),
'fieldtype': 'float',
'fieldname': 'opening_balance',
- 'width': 160,
+ 'width': 120,
+ }, {
+ 'label': _('New Allocation'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'new_allocation',
+ 'width': 120,
+ }, {
+ 'label': _('Expired Leaves'),
+ 'fieldtype': 'Float',
+ 'fieldname': 'expired_leaves',
+ 'width': 120,
}, {
'label': _('Leaves Taken'),
'fieldtype': 'float',
'fieldname': 'leaves_taken',
- 'width': 160,
+ 'width': 120,
}, {
'label': _('Closing Balance'),
'fieldtype': 'float',
'fieldname': 'closing_balance',
- 'width': 160,
+ 'width': 120,
}]
return columns
@@ -72,7 +83,7 @@ def get_data(filters):
'leave_type': leave_type
})
for employee in active_employees:
-
+
leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver)
if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \
@@ -82,16 +93,13 @@ def get_data(filters):
'employee_name': employee.employee_name
})
- leaves_taken = get_leaves_for_period(employee.name, leave_type,
- filters.from_date, filters.to_date) * -1
+ leave_details = calculate_leaves_details(filters, leave_type, employee)
+ row.opening_balance = flt(leave_details[0])
+ row.new_allocation = flt(leave_details[1])
+ row.expired_leaves = flt(leave_details[2])
+ row.leaves_taken = flt(leave_details[3])
+ row.closing_balance = flt(leave_details[4])
- opening = get_leave_balance_on(employee.name, leave_type, filters.from_date)
- closing = get_leave_balance_on(employee.name, leave_type, filters.to_date)
-
- row.opening_balance = opening
- row.leaves_taken = leaves_taken
- row.closing_balance = closing
- row.indent = 1
data.append(row)
return data
@@ -108,23 +116,3 @@ def get_conditions(filters):
return conditions
-def get_department_leave_approver_map(department=None):
- conditions=''
- if department:
- conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
-
- # get current department and all its child
- department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
-
- # retrieve approvers list from current department and from its subsequent child departments
- approver_list = frappe.get_all('Department Approver', filters={
- 'parentfield': 'leave_approvers',
- 'parent': ('in', department_list)
- }, fields=['parent', 'approver'], as_list=1)
-
- approvers = {}
-
- for k, v in approver_list:
- approvers.setdefault(k, []).append(v)
-
- return approvers
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 4f5653dc67e..5c95e000f9c 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -9,6 +9,8 @@ from frappe.model.document import Document
from frappe.desk.form import assign_to
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+class DuplicateDeclarationError(frappe.ValidationError): pass
+
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
@@ -226,6 +228,17 @@ def get_employee_leave_policy(employee):
else:
frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee))
+def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
+ existing_record = frappe.db.exists(doctype, {
+ "payroll_period": payroll_period,
+ "employee": employee,
+ 'docstatus': ['<', 2],
+ 'name': ['!=', docname]
+ })
+ if existing_record:
+ frappe.throw(_("{0} already exists for employee {1} and period {2}")
+ .format(doctype, employee, payroll_period), DuplicateDeclarationError)
+
def validate_tax_declaration(declarations):
subcategories = []
for d in declarations:
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 19d36f6e8c4..c898d378c3a 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -183,7 +183,7 @@ class BOM(WebsiteGenerator):
if self.rm_cost_as_per == 'Valuation Rate':
rate = self.get_valuation_rate(arg) * (arg.get("conversion_factor") or 1)
elif self.rm_cost_as_per == 'Last Purchase Rate':
- rate = (arg.get('last_purchase_rate') \
+ rate = flt(arg.get('last_purchase_rate') \
or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")) \
* (arg.get("conversion_factor") or 1)
elif self.rm_cost_as_per == "Price List":
@@ -236,12 +236,13 @@ class BOM(WebsiteGenerator):
if rate:
d.rate = rate
d.amount = flt(d.rate) * flt(d.qty)
+ d.db_update()
if self.docstatus == 1:
self.flags.ignore_validate_update_after_submit = True
self.calculate_cost()
if save:
- self.save()
+ self.db_update()
self.update_exploded_items()
# update parent BOMs
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
index 2758a423716..e6c10ad12b0 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
@@ -82,7 +82,7 @@ def enqueue_replace_bom(args):
@frappe.whitelist()
def enqueue_update_cost():
- frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost")
+ frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000)
frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes."))
def update_latest_price_in_all_boms():
@@ -98,6 +98,9 @@ def replace_bom(args):
doc.replace_bom()
def update_cost():
+ frappe.db.auto_commit_on_many_writes = 1
bom_list = get_boms_in_bottom_up_order()
for bom in bom_list:
- frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
\ No newline at end of file
+ frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
+
+ frappe.db.auto_commit_on_many_writes = 0
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index 154addf14e8..ac9a409bcbe 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -5,6 +5,9 @@
from __future__ import unicode_literals
import unittest
import frappe
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
test_records = frappe.get_test_records('BOM')
@@ -27,4 +30,31 @@ class TestBOMUpdateTool(unittest.TestCase):
# reverse, as it affects other testcases
update_tool.current_bom = bom_doc.name
update_tool.new_bom = current_bom
- update_tool.replace_bom()
\ No newline at end of file
+ update_tool.replace_bom()
+
+ def test_bom_cost(self):
+ for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
+ item_doc = create_item(item, valuation_rate=100)
+ if item_doc.valuation_rate != 100.00:
+ frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100)
+
+ bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name")
+ if not bom_no:
+ doc = make_bom(item = 'BOM Cost Test Item 1',
+ raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR")
+ else:
+ doc = frappe.get_doc("BOM", bom_no)
+
+ self.assertEquals(doc.total_cost, 200)
+
+ frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
+ update_cost()
+
+ doc.load_from_db()
+ self.assertEquals(doc.total_cost, 300)
+
+ frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
+ update_cost()
+
+ doc.load_from_db()
+ self.assertEquals(doc.total_cost, 200)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 9ee5e80f88d..bbce6f55d81 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -86,7 +86,9 @@ frappe.ui.form.on('Job Card', {
frm.set_value('current_time' , 0);
}
- frm.save();
+ frm.save("Save", () => {}, "", () => {
+ frm.doc.time_logs.pop(-1);
+ });
},
complete_job: function(frm, completed_time, completed_qty) {
@@ -109,6 +111,24 @@ frappe.ui.form.on('Job Card', {
});
},
+ validate: function(frm) {
+ if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
+ frm.trigger("reset_timer");
+ }
+ },
+
+ employee: function(frm) {
+ if (frm.doc.job_started && !frm.doc.current_time) {
+ frm.trigger("reset_timer");
+ }
+ },
+
+ reset_timer: function(frm) {
+ frm.set_value('started_time' , '');
+ frm.set_value('job_started', 0);
+ frm.set_value('current_time' , 0);
+ },
+
make_dashboard: function(frm) {
if(frm.doc.__islocal)
return;
@@ -209,5 +229,10 @@ frappe.ui.form.on('Job Card', {
frappe.ui.form.on('Job Card Time Log', {
completed_qty: function(frm) {
frm.events.set_total_completed_qty(frm);
+ },
+
+ to_time: function(frm) {
+ frm.set_value('job_started', 0);
+ frm.set_value('started_time', '');
}
})
\ 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 7c648c79014..4c1ab7479fb 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff
+from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff, get_link_to_form
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
@@ -90,11 +90,15 @@ class JobCard(Document):
def validate_job_card(self):
if not self.time_logs:
- frappe.throw(_("Time logs are required for job card {0}").format(self.name))
+ frappe.throw(_("Time logs are required for {0} {1}")
+ .format(frappe.bold("Job Card"), get_link_to_form("Job Card", self.name)))
if self.for_quantity and self.total_completed_qty != self.for_quantity:
- frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})"
- .format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity))))
+ total_completed_qty = frappe.bold(_("Total Completed Qty"))
+ qty_to_manufacture = frappe.bold(_("Qty to Manufacture"))
+
+ frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})"
+ .format(total_completed_qty, frappe.bold(self.total_completed_qty), qty_to_manufacture,frappe.bold(self.for_quantity))))
def update_work_order(self):
if not self.work_order:
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index f70c9cc43fc..26f580db339 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -192,9 +192,10 @@ def make_bom(**args):
args = frappe._dict(args)
bom = frappe.get_doc({
- 'doctype': "BOM",
+ 'doctype': 'BOM',
'is_default': 1,
'item': args.item,
+ 'currency': args.currency or 'USD',
'quantity': args.quantity or 1,
'company': args.company or '_Test Company'
})
@@ -211,4 +212,5 @@ def make_bom(**args):
})
bom.insert(ignore_permissions=True)
- bom.submit()
\ No newline at end of file
+ bom.submit()
+ return bom
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 1989c10c394..d14c8d82f11 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -122,6 +122,11 @@ frappe.ui.form.on("Work Order", {
}
},
+ source_warehouse: function(frm) {
+ let transaction_controller = new erpnext.TransactionController();
+ transaction_controller.autofill_warehouse(frm.doc.required_items, "source_warehouse", frm.doc.source_warehouse);
+ },
+
refresh: function(frm) {
erpnext.toggle_naming_series();
erpnext.work_order.set_custom_buttons(frm);
@@ -232,6 +237,8 @@ frappe.ui.form.on("Work Order", {
});
}, __("Job Card"), __("Create"));
+ dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide();
+
var pending_qty = 0;
frm.doc.operations.forEach(data => {
if(data.completed_qty != frm.doc.qty) {
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 65f4d08459a..75ebcbc971b 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -18,10 +18,10 @@ def get_columns():
"""return columns"""
columns = [
_("Item") + ":Link/Item:150",
- _("Description") + "::500",
- _("Qty per BOM Line") + ":Float:100",
- _("Required Qty") + ":Float:100",
- _("In Stock Qty") + ":Float:100",
+ _("Description") + "::300",
+ _("BOM Qty") + ":Float:160",
+ _("Required Qty") + ":Float:120",
+ _("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200",
]
@@ -59,13 +59,14 @@ def get_bom_stock(filters):
bom_item.item_code,
bom_item.description ,
bom_item.{qty_field},
- bom_item.{qty_field} * {qty_to_produce},
+ bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty,
- sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce})))
+ sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
FROM
- {table} AS bom_item
+ `tabBOM` AS bom INNER JOIN {table} AS bom_item
+ ON bom.name = bom_item.parent
LEFT JOIN `tabBin` AS ledger
- ON bom_item.item_code = ledger.item_code
+ ON bom_item.item_code = ledger.item_code
{conditions}
WHERE
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7cb92d228d9..dd3bc240917 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -651,8 +651,13 @@ erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.add_export_type_field_in_party_master
erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22
erpnext.patches.v12_0.create_irs_1099_field_united_states
+erpnext.patches.v12_0.add_permission_in_lower_deduction
erpnext.patches.v12_0.set_permission_einvoicing
erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom
erpnext.patches.v12_0.recalculate_requested_qty_in_bin
erpnext.patches.v12_0.rename_mws_settings_fields
erpnext.patches.v12_0.set_correct_status_for_expense_claim
+erpnext.patches.v12_0.set_updated_purpose_in_pick_list
+erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
+erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
+erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py
index 121a20288c3..83b2a4cc09e 100644
--- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py
+++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py
@@ -6,4 +6,6 @@ def execute():
if not company:
return
- add_permissions()
\ No newline at end of file
+ frappe.reload_doc("regional", "doctype", "lower_deduction_certificate")
+ frappe.reload_doc("regional", "doctype", "gstr_3b_report")
+ add_permissions()
diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py
index fa3605ba5f1..83fb53d2a73 100644
--- a/erpnext/patches/v11_0/set_salary_component_properties.py
+++ b/erpnext/patches/v11_0/set_salary_component_properties.py
@@ -5,8 +5,7 @@ def execute():
frappe.reload_doc('hr', 'doctype', 'salary_detail')
frappe.reload_doc('hr', 'doctype', 'salary_component')
- frappe.db.sql("update `tabSalary Component` set is_payable=1, is_tax_applicable=1 where type='Earning'")
- frappe.db.sql("update `tabSalary Component` set is_payable=0 where type='Deduction'")
+ frappe.db.sql("update `tabSalary Component` set is_tax_applicable=1 where type='Earning'")
frappe.db.sql("""update `tabSalary Component` set variable_based_on_taxable_salary=1
where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""")
diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py
new file mode 100644
index 00000000000..af9bf74f30e
--- /dev/null
+++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py
@@ -0,0 +1,13 @@
+import frappe
+from frappe.permissions import add_permission, update_permission_property
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ frappe.reload_doc('regional', 'doctype', 'Lower Deduction Certificate')
+
+ add_permission('Lower Deduction Certificate', 'Accounts Manager', 0)
+ update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1)
+ update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py
new file mode 100644
index 00000000000..13e935b2d39
--- /dev/null
+++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ warehouse_perm = frappe.get_all("User Permission",
+ fields=["count(*) as p_count", "is_default", "user"], filters={"allow": "Warehouse"}, group_by="user")
+
+ if not warehouse_perm:
+ return
+
+ execute_patch = False
+ for perm_data in warehouse_perm:
+ if perm_data.p_count == 1 or (perm_data.p_count > 1 and frappe.get_all("User Permission",
+ filters = {"user": perm_data.user, "allow": "warehouse", "is_default": 1}, limit=1)):
+ execute_patch = True
+ break
+
+ if not execute_patch: return
+
+ for doctype in ["Sales Invoice", "Delivery Note"]:
+ if not frappe.get_meta(doctype + ' Item').get_field("target_warehouse").hidden: continue
+
+ cond = ""
+ if doctype == "Sales Invoice":
+ cond = " AND parent_doc.update_stock = 1"
+
+ data = frappe.db.sql(""" SELECT parent_doc.name as name, child_doc.name as child_name
+ FROM
+ `tab{doctype}` parent_doc, `tab{doctype} Item` child_doc
+ WHERE
+ parent_doc.name = child_doc.parent AND parent_doc.docstatus < 2
+ AND child_doc.target_warehouse is not null AND child_doc.target_warehouse != ''
+ AND child_doc.creation > '2020-04-16' {cond}
+ """.format(doctype=doctype, cond=cond), as_dict=1)
+
+ if data:
+ names = [d.child_name for d in data]
+ frappe.db.sql(""" UPDATE `tab{0} Item` set target_warehouse = null
+ WHERE name in ({1}) """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names))
+
+ frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null
+ WHERE parenttype = '{0}' and parent_detail_docname in ({1})
+ """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names))
+
+ parent_names = list(set([d.name for d in data]))
+
+ for d in parent_names:
+ doc = frappe.get_doc(doctype, d)
+ if doc.docstatus != 1: continue
+
+ doc.docstatus = 2
+ doc.update_stock_ledger()
+ doc.make_gl_entries_on_cancel(repost_future_gle=False)
+
+ # update stock & gl entries for submit state of PR
+ doc.docstatus = 1
+ doc.update_stock_ledger()
+ doc.make_gl_entries()
+
+ if frappe.get_meta('Sales Order Item').get_field("target_warehouse").hidden:
+ frappe.db.sql(""" UPDATE `tabSales Order Item` set target_warehouse = null
+ WHERE creation > '2020-04-16' and docstatus < 2 """)
+
+ frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null
+ WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """)
+
+
+
diff --git a/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py b/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py
index 1fefc90680e..c3a87fc5ca7 100644
--- a/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py
+++ b/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py
@@ -4,8 +4,11 @@
import frappe
def execute():
+
+ frappe.reload_doc("hr", "doctype", "expense_claim")
+
frappe.db.sql("""
update `tabExpense Claim`
set status = 'Paid'
where total_advance_amount + total_amount_reimbursed = total_sanctioned_amount + total_taxes_and_charges
- """)
\ No newline at end of file
+ """)
diff --git a/erpnext/patches/v12_0/set_permission_einvoicing.py b/erpnext/patches/v12_0/set_permission_einvoicing.py
index 1095c8c43e7..e2235105f94 100644
--- a/erpnext/patches/v12_0/set_permission_einvoicing.py
+++ b/erpnext/patches/v12_0/set_permission_einvoicing.py
@@ -10,6 +10,8 @@ def execute():
make_custom_fields()
+ frappe.reload_doc("regional", "doctype", "import_supplier_invoice")
+
add_permission('Import Supplier Invoice', 'Accounts Manager', 0)
update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'write', 1)
- update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1)
\ No newline at end of file
+ update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1)
diff --git a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py
new file mode 100644
index 00000000000..63ca540a8e2
--- /dev/null
+++ b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+def execute():
+ frappe.reload_doc("stock", "doctype", "pick_list")
+ frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery'
+ WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py
new file mode 100644
index 00000000000..f6561471de4
--- /dev/null
+++ b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py
@@ -0,0 +1,24 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import add_days, getdate, today
+
+def execute():
+ if frappe.db.exists('DocType', 'Email Campaign'):
+ email_campaign = frappe.get_all('Email Campaign')
+ for campaign in email_campaign:
+ doc = frappe.get_doc("Email Campaign",campaign["name"])
+ send_after_days = []
+
+ camp = frappe.get_doc("Campaign", doc.campaign_name)
+ for entry in camp.get("campaign_schedules"):
+ send_after_days.append(entry.send_after_days)
+ if send_after_days:
+ end_date = add_days(getdate(doc.start_date), max(send_after_days))
+ doc.db_set("end_date", end_date)
+ today_date = getdate(today())
+ if doc.start_date > today_date:
+ doc.db_set("status", "Scheduled")
+ elif end_date >= today_date:
+ doc.db_set("status", "In Progress")
+ elif end_date < today_date:
+ doc.db_set("status", "Completed")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py
new file mode 100644
index 00000000000..179be2cfde5
--- /dev/null
+++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py
@@ -0,0 +1,99 @@
+# 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():
+ if not frappe.db.table_exists("Payroll Period"):
+ return
+
+ for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"):
+ frappe.reload_doc("hr", "doctype", doctype)
+
+
+ for company in frappe.get_all("Company"):
+ payroll_periods = frappe.db.sql("""
+ SELECT
+ name, start_date, end_date, standard_tax_exemption_amount
+ FROM
+ `tabPayroll Period`
+ WHERE company=%s
+ ORDER BY start_date DESC
+ """, company.name, as_dict = 1)
+
+ for i, period in enumerate(payroll_periods):
+ income_tax_slab = frappe.new_doc("Income Tax Slab")
+ income_tax_slab.name = "Tax Slab:" + period.name
+
+ if i == 0:
+ income_tax_slab.disabled = 0
+ else:
+ income_tax_slab.disabled = 1
+
+ income_tax_slab.effective_from = period.start_date
+ income_tax_slab.company = company.name
+ income_tax_slab.allow_tax_exemption = 1
+ income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount
+
+ income_tax_slab.flags.ignore_mandatory = True
+ income_tax_slab.submit()
+
+ frappe.db.sql(
+ """ UPDATE `tabTaxable Salary Slab`
+ SET parent = %s , parentfield = 'slabs' , parenttype = "Income Tax Slab"
+ WHERE parent = %s
+ """, (income_tax_slab.name, period.name), as_dict = 1)
+
+ if i == 0:
+ frappe.db.sql("""
+ UPDATE
+ `tabSalary Structure Assignment`
+ set
+ income_tax_slab = %s
+ where
+ company = %s
+ and from_date >= %s
+ and docstatus < 2
+ """, (income_tax_slab.name, company.name, period.start_date))
+
+ # move other incomes to separate document
+ migrated = []
+ proofs = frappe.get_all("Employee Tax Exemption Proof Submission",
+ filters = {'docstatus': 1},
+ fields =['payroll_period', 'employee', 'company', 'income_from_other_sources']
+ )
+ for proof in proofs:
+ if proof.income_from_other_sources:
+ employee_other_income = frappe.new_doc("Employee Other Income")
+ employee_other_income.employee = proof.employee
+ employee_other_income.payroll_period = proof.payroll_period
+ employee_other_income.company = proof.company
+ employee_other_income.amount = proof.income_from_other_sources
+
+ try:
+ employee_other_income.submit()
+ migrated.append([proof.employee, proof.payroll_period])
+ except:
+ pass
+
+ declerations = frappe.get_all("Employee Tax Exemption Declaration",
+ filters = {'docstatus': 1},
+ fields =['payroll_period', 'employee', 'company', 'income_from_other_sources']
+ )
+
+ for declaration in declerations:
+ if declaration.income_from_other_sources \
+ and [declaration.employee, declaration.payroll_period] not in migrated:
+ employee_other_income = frappe.new_doc("Employee Other Income")
+ employee_other_income.employee = declaration.employee
+ employee_other_income.payroll_period = declaration.payroll_period
+ employee_other_income.company = declaration.company
+ employee_other_income.amount = declaration.income_from_other_sources
+
+ try:
+ employee_other_income.submit()
+ except:
+ pass
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index afbdbc661d3..802cc056c6a 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -253,6 +253,13 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
}
},
+ rejected_warehouse: function(doc, cdt) {
+ // trigger autofill_warehouse only if parent rejected_warehouse field is triggered
+ if (["Purchase Invoice", "Purchase Receipt"].includes(cdt)) {
+ this.autofill_warehouse(doc.items, "rejected_warehouse", doc.rejected_warehouse);
+ }
+ },
+
category: function(doc, cdt, cdn) {
// should be the category field of tax table
if(cdt != doc.doctype) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 5312bc06ecc..4b40c8d4c75 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -175,6 +175,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
};
}
+ if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) {
+ this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) {
+ var item = locals[cdt][cdn];
+ return {
+ query: "erpnext.controllers.queries.get_blanket_orders",
+ filters: {
+ "company": doc.company,
+ "blanket_order_type": doc.doctype === "Sales Order" ? "Selling" : "Purchasing",
+ "item": item.item_code
+ }
+ }
+ });
+ }
+
},
onload: function() {
var me = this;
@@ -499,7 +513,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
weight_uom: item.weight_uom,
- uom : item.uom,
manufacturer: item.manufacturer,
stock_uom: item.stock_uom,
pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
@@ -538,6 +551,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!d[k]) d[k] = v;
});
+ if (d.has_batch_no && d.has_serial_no) {
+ d.batch_no = undefined;
+ }
+
erpnext.show_serial_batch_selector(me.frm, d, (item) => {
me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse)
@@ -1371,7 +1388,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
me.frm.doc.items.forEach(d => {
if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) {
for(var k in data) {
- if (in_list(fields, k) && data[k]) {
+ if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) {
frappe.model.set_value(d.doctype, d.name, k, data[k]);
}
}
@@ -1392,6 +1409,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
for (let key in free_item_data) {
row_to_modify[key] = free_item_data[key];
}
+ } if (items && items.length && free_item_data) {
+ items[0].qty = free_item_data.qty
}
},
@@ -1838,21 +1857,16 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
},
set_reserve_warehouse: function() {
- this.autofill_warehouse("reserve_warehouse");
+ this.autofill_warehouse(this.frm.doc.supplied_items, "reserve_warehouse", this.frm.doc.set_reserve_warehouse);
},
set_warehouse: function() {
- this.autofill_warehouse("warehouse");
+ this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
},
- autofill_warehouse : function (warehouse_field) {
- // set warehouse in all child table rows
- var me = this;
- let warehouse = (warehouse_field === "warehouse") ? me.frm.doc.set_warehouse : me.frm.doc.set_reserve_warehouse;
- let child_table = (warehouse_field === "warehouse") ? me.frm.doc.items : me.frm.doc.supplied_items;
- let doctype = (warehouse_field === "warehouse") ? (me.frm.doctype + " Item") : (me.frm.doctype + " Item Supplied");
-
- if(warehouse) {
+ autofill_warehouse : function (child_table, warehouse_field, warehouse) {
+ if (warehouse && child_table && child_table.length) {
+ let doctype = child_table[0].doctype;
$.each(child_table || [], function(i, item) {
frappe.model.set_value(doctype, item.name, warehouse_field, warehouse);
});
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 0a352648304..224f4f5525d 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -436,6 +436,44 @@ erpnext.utils.update_child_items = function(opts) {
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
this.data = [];
+ const fields = [{
+ fieldtype:'Data',
+ fieldname:"docname",
+ read_only: 1,
+ hidden: 1,
+ }, {
+ fieldtype:'Link',
+ fieldname:"item_code",
+ options: 'Item',
+ in_list_view: 1,
+ read_only: 0,
+ disabled: 0,
+ label: __('Item Code')
+ }, {
+ fieldtype:'Float',
+ fieldname:"qty",
+ default: 0,
+ read_only: 0,
+ in_list_view: 1,
+ label: __('Qty')
+ }, {
+ fieldtype:'Currency',
+ fieldname:"rate",
+ default: 0,
+ read_only: 0,
+ in_list_view: 1,
+ label: __('Rate')
+ }];
+
+ if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
+ fields.splice(2, 0, {
+ fieldtype: 'Date',
+ fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date",
+ in_list_view: 1,
+ label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date")
+ })
+ }
+
const dialog = new frappe.ui.Dialog({
title: __("Update Items"),
fields: [
@@ -450,34 +488,7 @@ erpnext.utils.update_child_items = function(opts) {
get_data: () => {
return this.data;
},
- fields: [{
- fieldtype:'Data',
- fieldname:"docname",
- read_only: 1,
- hidden: 1,
- }, {
- fieldtype:'Link',
- fieldname:"item_code",
- options: 'Item',
- in_list_view: 1,
- read_only: 0,
- disabled: 0,
- label: __('Item Code')
- }, {
- fieldtype:'Float',
- fieldname:"qty",
- default: 0,
- read_only: 0,
- in_list_view: 1,
- label: __('Qty')
- }, {
- fieldtype:'Currency',
- fieldname:"rate",
- default: 0,
- read_only: 0,
- in_list_view: 1,
- label: __('Rate')
- }]
+ fields: fields
},
],
primary_action: function() {
@@ -506,6 +517,8 @@ erpnext.utils.update_child_items = function(opts) {
"docname": d.name,
"name": d.name,
"item_code": d.item_code,
+ "delivery_date": d.delivery_date,
+ "schedule_date": d.schedule_date,
"qty": d.qty,
"rate": d.rate,
});
diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js
index 75c5a820b46..b6720c05cb2 100644
--- a/erpnext/public/js/utils/dimension_tree_filter.js
+++ b/erpnext/public/js/utils/dimension_tree_filter.js
@@ -24,7 +24,7 @@ doctypes_with_dimensions.forEach((doctype) => {
onload: function(frm) {
erpnext.dimension_filters.forEach((dimension) => {
frappe.model.with_doctype(dimension['document_type'], () => {
- if (frappe.meta.has_field(dimension['document_type'], 'is_group')) {
+ if(frappe.meta.has_field(dimension['document_type'], 'is_group')) {
frm.set_query(dimension['fieldname'], {
"is_group": 0
});
@@ -42,19 +42,21 @@ doctypes_with_dimensions.forEach((doctype) => {
update_dimension: function(frm) {
erpnext.dimension_filters.forEach((dimension) => {
- if (frm.is_new()) {
- if (frm.doc.company && Object.keys(default_dimensions || {}).length > 0
+ if(frm.is_new()) {
+ if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0
&& default_dimensions[frm.doc.company]) {
- if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
- frm.set_value(dimension['fieldname'],
- default_dimensions[frm.doc.company][dimension['document_type']]);
- }
+ let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']];
- $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) {
- frappe.model.set_value(row.doctype, row.name, dimension['fieldname'],
- default_dimensions[frm.doc.company][dimension['document_type']])
- });
+ if(default_dimension) {
+ if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
+ frm.set_value(dimension['fieldname'], default_dimension);
+ }
+
+ $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) {
+ frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], default_dimension);
+ });
+ }
}
}
});
@@ -71,20 +73,6 @@ child_docs.forEach((doctype) => {
});
},
- accounts_add: function(frm, cdt, cdn) {
- erpnext.dimension_filters.forEach((dimension) => {
- var row = frappe.get_doc(cdt, cdn);
- frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]);
- });
- },
-
- items_add: function(frm, cdt, cdn) {
- erpnext.dimension_filters.forEach((dimension) => {
- var row = frappe.get_doc(cdt, cdn);
- frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]);
- });
- },
-
accounts_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn);
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json
index 6860ed3fdae..caed7367dc1 100644
--- a/erpnext/regional/doctype/datev_settings/datev_settings.json
+++ b/erpnext/regional/doctype/datev_settings/datev_settings.json
@@ -28,6 +28,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID",
+ "length": 5,
"reqd": 1
},
{
@@ -42,6 +43,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Consultant ID",
+ "length": 7,
"reqd": 1
},
{
@@ -57,7 +59,7 @@
"fieldtype": "Column Break"
}
],
- "modified": "2019-08-14 00:03:26.616460",
+ "modified": "2020-04-15 12:59:57.786506",
"modified_by": "Administrator",
"module": "Regional",
"name": "DATEV Settings",
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/__init__.py b/erpnext/regional/doctype/lower_deduction_certificate/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js
new file mode 100644
index 00000000000..8257bf8a969
--- /dev/null
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Lower Deduction Certificate', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
new file mode 100644
index 00000000000..f48fe6f4763
--- /dev/null
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
@@ -0,0 +1,138 @@
+{
+ "actions": [],
+ "autoname": "field:certificate_no",
+ "creation": "2020-03-10 23:12:10.072631",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "certificate_details_section",
+ "section_code",
+ "fiscal_year",
+ "column_break_3",
+ "certificate_no",
+ "section_break_3",
+ "supplier",
+ "column_break_7",
+ "pan_no",
+ "validity_details_section",
+ "valid_from",
+ "column_break_10",
+ "valid_upto",
+ "section_break_9",
+ "rate",
+ "column_break_14",
+ "certificate_limit"
+ ],
+ "fields": [
+ {
+ "fieldname": "certificate_no",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Certificate No",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "section_code",
+ "fieldtype": "Select",
+ "label": "Section Code",
+ "options": "192\n193\n194\n194A\n194C\n194D\n194H\n194I\n194J\n194LA\n194LBB\n194LBC\n195",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break",
+ "label": "Deductee Details"
+ },
+ {
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Supplier",
+ "options": "Supplier",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "supplier.pan",
+ "fetch_if_empty": 1,
+ "fieldname": "pan_no",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "PAN No",
+ "reqd": 1
+ },
+ {
+ "fieldname": "validity_details_section",
+ "fieldtype": "Section Break",
+ "label": "Validity Details"
+ },
+ {
+ "fieldname": "valid_upto",
+ "fieldtype": "Date",
+ "label": "Valid Upto",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "rate",
+ "fieldtype": "Percent",
+ "label": "Rate Of TDS As Per Certificate",
+ "reqd": 1
+ },
+ {
+ "fieldname": "certificate_limit",
+ "fieldtype": "Currency",
+ "label": "Certificate Limit",
+ "reqd": 1
+ },
+ {
+ "fieldname": "certificate_details_section",
+ "fieldtype": "Section Break",
+ "label": "Certificate Details"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "valid_from",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Valid From",
+ "reqd": 1
+ },
+ {
+ "fieldname": "fiscal_year",
+ "fieldtype": "Link",
+ "label": "Fiscal Year",
+ "options": "Fiscal Year",
+ "reqd": 1
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-23 23:04:41.203721",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Lower Deduction Certificate",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
new file mode 100644
index 00000000000..e8a8ed87505
--- /dev/null
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
@@ -0,0 +1,26 @@
+# -*- 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.utils import getdate
+from frappe.model.document import Document
+from erpnext.accounts.utils import get_fiscal_year
+
+class LowerDeductionCertificate(Document):
+ def validate(self):
+ if getdate(self.valid_upto) < getdate(self.valid_from):
+ frappe.throw(_("Valid Upto date cannot be before Valid From date"))
+
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ if not (fiscal_year.year_start_date <= getdate(self.valid_from) \
+ <= fiscal_year.year_end_date):
+ frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
+
+ if not (fiscal_year.year_start_date <= getdate(self.valid_upto) \
+ <= fiscal_year.year_end_date):
+ frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
+
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py
new file mode 100644
index 00000000000..7e950206fcc
--- /dev/null
+++ b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.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 TestLowerDeductionCertificate(unittest.TestCase):
+ pass
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 1957e7fecca..77a466fdff7 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -77,7 +77,7 @@ def add_custom_roles_for_reports():
)).insert()
def add_permissions():
- for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report'):
+ for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0)
@@ -530,12 +530,18 @@ def make_fixtures(company=None):
def set_salary_components(docs):
docs.extend([
- {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', 'description': 'Professional Tax', 'type': 'Deduction'},
- {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', 'description': 'Provident fund', 'type': 'Deduction'},
- {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', 'description': 'House Rent Allowance', 'type': 'Earning'},
- {'doctype': 'Salary Component', 'salary_component': 'Basic', 'description': 'Basic', 'type': 'Earning'},
- {'doctype': 'Salary Component', 'salary_component': 'Arrear', 'description': 'Arrear', 'type': 'Earning'},
- {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', 'description': 'Leave Encashment', 'type': 'Earning'}
+ {'doctype': 'Salary Component', 'salary_component': 'Professional Tax',
+ 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1},
+ {'doctype': 'Salary Component', 'salary_component': 'Provident Fund',
+ 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1},
+ {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance',
+ 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1},
+ {'doctype': 'Salary Component', 'salary_component': 'Basic',
+ 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1},
+ {'doctype': 'Salary Component', 'salary_component': 'Arrear',
+ 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1},
+ {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment',
+ 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1}
])
def set_tax_withholding_category(company):
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 831985fc7cc..e518c14a8a0 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -273,7 +273,7 @@ def calculate_annual_eligible_hra_exemption(doc):
})
def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component):
- salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1)
+ salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True)
basic_amt, hra_amt = 0, 0
for earning in salary_slip.earnings:
if earning.salary_component == basic_component:
@@ -360,8 +360,6 @@ def get_ewb_data(dt, dn):
if dt != 'Sales Invoice':
frappe.throw(_('e-Way Bill JSON can only be generated from Sales Invoice'))
- dn = dn.split(',')
-
ewaybills = []
for doc_name in dn:
doc = frappe.get_doc(dt, doc_name)
@@ -439,16 +437,22 @@ def get_ewb_data(dt, dn):
@frappe.whitelist()
def generate_ewb_json(dt, dn):
+ dn = json.loads(dn)
+ return get_ewb_data(dt, dn)
- data = get_ewb_data(dt, dn)
+@frappe.whitelist()
+def download_ewb_json():
+ data = frappe._dict(frappe.local.form_dict)
- frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True)
+ frappe.local.response.filecontent = json.dumps(data['data'], indent=4, sort_keys=True)
frappe.local.response.type = 'download'
- if len(data['billLists']) > 1:
+ billList = json.loads(data['data'])['billLists']
+
+ if len(billList) > 1:
doc_name = 'Bulk'
else:
- doc_name = dn
+ doc_name = data['docname']
frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5))
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index a58e13a0947..a2d74b889ad 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -12,6 +12,7 @@ import datetime
import json
import six
from six import string_types
+from csv import QUOTE_NONNUMERIC
import frappe
from frappe import _
@@ -57,8 +58,8 @@ def get_columns():
"fieldtype": "Data",
},
{
- "label": "Kontonummer",
- "fieldname": "Kontonummer",
+ "label": "Konto",
+ "fieldname": "Konto",
"fieldtype": "Data",
},
{
@@ -71,6 +72,11 @@ def get_columns():
"fieldname": "Belegdatum",
"fieldtype": "Date",
},
+ {
+ "label": "Belegfeld 1",
+ "fieldname": "Belegfeld 1",
+ "fieldtype": "Data",
+ },
{
"label": "Buchungstext",
"fieldname": "Buchungstext",
@@ -79,22 +85,26 @@ def get_columns():
{
"label": "Beleginfo - Art 1",
"fieldname": "Beleginfo - Art 1",
- "fieldtype": "Data",
+ "fieldtype": "Link",
+ "options": "DocType"
},
{
"label": "Beleginfo - Inhalt 1",
"fieldname": "Beleginfo - Inhalt 1",
- "fieldtype": "Data",
+ "fieldtype": "Dynamic Link",
+ "options": "Beleginfo - Art 1"
},
{
"label": "Beleginfo - Art 2",
"fieldname": "Beleginfo - Art 2",
- "fieldtype": "Data",
+ "fieldtype": "Link",
+ "options": "DocType"
},
{
"label": "Beleginfo - Inhalt 2",
"fieldname": "Beleginfo - Inhalt 2",
- "fieldtype": "Data",
+ "fieldtype": "Dynamic Link",
+ "options": "Beleginfo - Art 2"
}
]
@@ -122,13 +132,14 @@ def get_gl_entries(filters, as_dict):
case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen',
/* account number or, if empty, party account number */
- coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer',
+ coalesce(acc.account_number, acc_pa.account_number) as 'Konto',
/* against number or, if empty, party against number */
coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)',
gl.posting_date as 'Belegdatum',
- gl.remarks as 'Buchungstext',
+ gl.voucher_no as 'Belegfeld 1',
+ LEFT(gl.remarks, 60) as 'Buchungstext',
gl.voucher_type as 'Beleginfo - Art 1',
gl.voucher_no as 'Beleginfo - Inhalt 1',
gl.against_voucher_type as 'Beleginfo - Art 2',
@@ -177,16 +188,19 @@ def get_datev_csv(data, filters):
data -- array of dictionaries
filters -- dict
"""
+ coa = frappe.get_value("Company", filters.get("company"), "chart_of_accounts")
+ coa_used = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "")
+
header = [
- # A = DATEV format
+ # A = DATEV-Format-KZ
# DTVF = created by DATEV software,
# EXTF = created by other software
- "EXTF",
+ '"EXTF"',
# B = version of the DATEV format
# 141 = 1.41,
# 510 = 5.10,
# 720 = 7.20
- "510",
+ "700",
# C = Data category
# 21 = Transaction batch (Buchungsstapel),
# 67 = Buchungstextkonstanten,
@@ -200,23 +214,22 @@ def get_datev_csv(data, filters):
# Kontenbeschriftungen
"Buchungsstapel",
# E = Format version (regarding format name)
- "",
+ "9",
# F = Generated on
- datetime.datetime.now().strftime("%Y%m%d"),
+ datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000',
# G = Imported on -- stays empty
"",
- # H = Origin (SV = other (?), RE = KARE)
- "SV",
+ # H = Herkunfts-Kennzeichen (Origin)
+ # Any two letters
+ '"EN"',
# I = Exported by
- frappe.session.user,
+ '"%s"' % frappe.session.user,
# J = Imported by -- stays empty
"",
# K = Tax consultant number (Beraternummer)
frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number") or "",
- "",
# L = Tax client number (Mandantennummer)
frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "",
- "",
# M = Start of the fiscal year (Wirtschaftsjahresbeginn)
frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"),
# N = Length of account numbers (Sachkontenlänge)
@@ -226,10 +239,7 @@ def get_datev_csv(data, filters):
# P = Transaction batch end date (YYYYMMDD)
frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"),
# Q = Description (for example, "January - February 2019 Transactions")
- "{} - {} Buchungsstapel".format(
- frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"),
- frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy")
- ),
+ "Buchungsstapel",
# R = Diktatkürzel
"",
# S = Buchungstyp
@@ -237,11 +247,29 @@ def get_datev_csv(data, filters):
# 2 = Annual financial statement (Jahresabschluss)
"1",
# T = Rechnungslegungszweck
- "",
+ "0", # vom Rechnungslegungszweck unabhängig
# U = Festschreibung
- "",
+ "0", # keine Festschreibung
# V = Kontoführungs-Währungskennzeichen des Geldkontos
- frappe.get_value("Company", filters.get("company"), "default_currency")
+ frappe.get_value("Company", filters.get("company"), "default_currency"),
+ # reserviert
+ '',
+ # Derivatskennzeichen
+ '',
+ # reserviert
+ '',
+ # reserviert
+ '',
+ # SKR
+ '"%s"' % coa_used,
+ # Branchen-Lösungs-ID
+ '',
+ # reserviert
+ '',
+ # reserviert
+ '',
+ # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung)
+ ''
]
columns = [
# All possible columns must tbe listed here, because DATEV requires them to
@@ -255,24 +283,27 @@ def get_datev_csv(data, filters):
"Basis-Umsatz",
"WKZ Basis-Umsatz",
# Konto/Gegenkonto
- "Kontonummer",
+ "Konto",
"Gegenkonto (ohne BU-Schlüssel)",
"BU-Schlüssel",
# Datum
"Belegdatum",
- # Belegfelder
+ # Rechnungs- / Belegnummer
"Belegfeld 1",
+ # z.B. Fälligkeitsdatum Format: TTMMJJ
"Belegfeld 2",
- # Weitere Felder
+ # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig)
"Skonto",
+ # Beschreibung des Buchungssatzes
"Buchungstext",
- # OPOS-Informationen
+ # Mahn- / Zahl-Sperre (1 = Postensperre)
"Postensperre",
"Diverse Adressnummer",
"Geschäftspartnerbank",
"Sachverhalt",
+ # Keine Mahnzinsen
"Zinssperre",
- # Digitaler Beleg
+ # Link auf den Buchungsbeleg (Programmkürzel + GUID)
"Beleglink",
# Beleginfo
"Beleginfo - Art 1",
@@ -291,22 +322,30 @@ def get_datev_csv(data, filters):
"Beleginfo - Inhalt 7",
"Beleginfo - Art 8",
"Beleginfo - Inhalt 8",
- # Kostenrechnung
- "Kost 1 - Kostenstelle",
- "Kost 2 - Kostenstelle",
- "Kost-Menge",
- # Steuerrechnung
- "EU-Land u. UStID",
+ # Zuordnung des Geschäftsvorfalls für die Kostenrechnung
+ "KOST1 - Kostenstelle",
+ "KOST2 - Kostenstelle",
+ "KOST-Menge",
+ # USt-ID-Nummer (Beispiel: DE133546770)
+ "EU-Mitgliedstaat u. USt-IdNr.",
+ # Der im EU-Bestimmungsland gültige Steuersatz
"EU-Steuersatz",
+ # I = Ist-Versteuerung,
+ # K = keine Umsatzsteuerrechnung
+ # P = Pauschalierung (z. B. für Land- und Forstwirtschaft),
+ # S = Soll-Versteuerung
"Abw. Versteuerungsart",
- # L+L Sachverhalt
+ # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG
"Sachverhalt L+L",
+ # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%)
"Funktionsergänzung L+L",
- # Funktion Steuerschlüssel 49
+ # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der
+ # steuerliche Sachverhalt mitgegeben werden
"BU 49 Hauptfunktionstyp",
"BU 49 Hauptfunktionsnummer",
"BU 49 Funktionsergänzung",
- # Zusatzinformationen
+ # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können
+ # frei erfasst werden.
"Zusatzinformation - Art 1",
"Zusatzinformation - Inhalt 1",
"Zusatzinformation - Art 2",
@@ -347,54 +386,76 @@ def get_datev_csv(data, filters):
"Zusatzinformation - Inhalt 19",
"Zusatzinformation - Art 20",
"Zusatzinformation - Inhalt 20",
- # Mengenfelder LuF
+ # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus,
+ # für andere SKR werden die Felder beim Import / Export überlesen bzw.
+ # leer exportiert.
"Stück",
"Gewicht",
- # Forderungsart
+ # 1 = Lastschrift
+ # 2 = Mahnung
+ # 3 = Zahlung
"Zahlweise",
"Forderungsart",
+ # JJJJ
"Veranlagungsjahr",
+ # TTMMJJJJ
"Zugeordnete Fälligkeit",
- # Weitere Felder
+ # 1 = Einkauf von Waren
+ # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen
"Skontotyp",
- # Anzahlungen
+ # Allgemeine Bezeichnung, des Auftrags / Projekts.
"Auftragsnummer",
+ # AA = Angeforderte Anzahlung / Abschlagsrechnung
+ # AG = Erhaltene Anzahlung (Geldeingang)
+ # AV = Erhaltene Anzahlung (Verbindlichkeit)
+ # SR = Schlussrechnung
+ # SU = Schlussrechnung (Umbuchung)
+ # SG = Schlussrechnung (Geldeingang)
+ # SO = Sonstige
"Buchungstyp",
"USt-Schlüssel (Anzahlungen)",
- "EU-Land (Anzahlungen)",
+ "EU-Mitgliedstaat (Anzahlungen)",
"Sachverhalt L+L (Anzahlungen)",
"EU-Steuersatz (Anzahlungen)",
"Erlöskonto (Anzahlungen)",
- # Stapelinformationen
+ # Wird beim Import durch SV (Stapelverarbeitung) ersetzt.
"Herkunft-Kz",
- # Technische Identifikation
- "Buchungs GUID",
- # Kostenrechnung
- "Kost-Datum",
- # OPOS-Informationen
+ # Wird von DATEV verwendet.
+ "Leerfeld",
+ # Format TTMMJJJJ
+ "KOST-Datum",
+ # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats
+ # (z.B. Rechnungs- oder Kundennummer).
"SEPA-Mandatsreferenz",
+ # 1 = Skontosperre
+ # 0 = Keine Skontosperre
"Skontosperre",
# Gesellschafter und Sonderbilanzsachverhalt
"Gesellschaftername",
+ # Amtliche Nummer aus der Feststellungserklärung
"Beteiligtennummer",
"Identifikationsnummer",
"Zeichnernummer",
- # OPOS-Informationen
+ # Format TTMMJJJJ
"Postensperre bis",
# Gesellschafter und Sonderbilanzsachverhalt
"Bezeichnung SoBil-Sachverhalt",
"Kennzeichen SoBil-Buchung",
- # Stapelinformationen
+ # 0 = keine Festschreibung
+ # 1 = Festschreibung
"Festschreibung",
- # Datum
+ # Format TTMMJJJJ
"Leistungsdatum",
+ # Format TTMMJJJJ
"Datum Zuord. Steuerperiode",
- # OPOS-Informationen
+ # OPOS-Informationen, Format TTMMJJJJ
"Fälligkeit",
- # Konto/Gegenkonto
+ # G oder 1 = Generalumkehr
+ # 0 = keine Generalumkehr
"Generalumkehr (GU)",
# Steuersatz für Steuerschlüssel
"Steuersatz",
+ # Beispiel: DE für Deutschland
"Land"
]
@@ -419,7 +480,9 @@ def get_datev_csv(data, filters):
# Do not number rows
index=False,
# Use all columns defined above
- columns=columns
+ columns=columns,
+ # Quote most fields, even currency values with "," separator
+ quoting=QUOTE_NONNUMERIC
)
if not six.PY2:
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index fa765dfaad9..b4e151b2e30 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -65,15 +65,6 @@ frappe.ui.form.on("Sales Order", {
}
});
- frm.set_query("blanket_order", "items", function() {
- return {
- filters: {
- "company": frm.doc.company,
- "docstatus": 1
- }
- }
- });
-
erpnext.queries.setup_warehouse_query(frm);
},
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 54e87f7a3b5..6462d3bc8c3 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -60,9 +60,9 @@
"base_total",
"base_net_total",
"column_break_33",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_38",
@@ -1196,7 +1196,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-30 19:15:28.605085",
+ "modified": "2020-04-17 12:50:39.640534",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ef2d19ac546..05e4aa892b0 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -1036,7 +1036,7 @@ def create_pick_list(source_name, target_doc=None):
},
}, target_doc)
- doc.purpose = 'Delivery against Sales Order'
+ doc.purpose = 'Delivery'
doc.set_item_locations()
diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
index 28dd0564075..aa57665a815 100644
--- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
+++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
@@ -53,10 +53,11 @@ def execute(filters=None):
new[1], repeat[1], new[1] + repeat[1]])
return [
- _("Year"), _("Month"),
- _("New Customers") + ":Int",
- _("Repeat Customers") + ":Int",
- _("Total") + ":Int",
+ _("Year") + "::100",
+ _("Month") + "::100",
+ _("New Customers") + ":Int:100",
+ _("Repeat Customers") + ":Int:100",
+ _("Total") + ":Int:100",
_("New Customer Revenue") + ":Currency:150",
_("Repeat Customer Revenue") + ":Currency:150",
_("Total Revenue") + ":Currency:150"
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index af100692c6f..4a7dd5ad9b4 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -228,9 +228,15 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
warehouse: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
+
+ if (item.serial_no && item.qty === item.serial_no.split(`\n`).length) {
+ return;
+ }
+
if (item.serial_no && !item.batch_no) {
item.serial_no = null;
}
+
var has_batch_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
has_batch_no = r && r.has_batch_no;
@@ -423,7 +429,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
if (doc.has_serial_no && doc.serial_no) {
args['serial_no'] = doc.serial_no
}
-
+
return frappe.call({
method: 'erpnext.stock.doctype.batch.batch.get_batch_no',
args: args,
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 2ee68723789..62aebbaf504 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -267,6 +267,14 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
frappe.ui.form.is_saving = false;
}
})
+ },
+
+ to_warehouse: function() {
+ let packed_items_table = this.frm.doc["packed_items"];
+ this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse);
+ if (packed_items_table && packed_items_table.length) {
+ this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse);
+ }
}
});
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 86200ba26b6..424a7f3c22f 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -66,9 +66,9 @@
"base_total",
"base_net_total",
"column_break_33",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_section",
"tax_category",
"column_break_39",
@@ -1240,7 +1240,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-30 19:17:13.122644",
+ "modified": "2020-04-17 12:51:41.288600",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 47a72b21a69..d7a93fb6917 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -434,15 +434,6 @@ class TestDeliveryNote(unittest.TestCase):
update_delivery_note_status(dn.name, "Closed")
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
- def test_customer_provided_parts_dn(self):
- create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- dn = create_delivery_note(item_code='CUST-0987', rate=0)
- self.assertEqual(dn.get("items")[0].allow_zero_valuation_rate, 1)
-
- # test if Delivery Note with rate is allowed against Customer Provided Item
- dn2 = create_delivery_note(item_code='CUST-0987', do_not_save=True)
- self.assertRaises(frappe.ValidationError, dn2.save)
-
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order()
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index c11f59fd81e..5c6718bb4d5 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -174,12 +174,11 @@ class MaterialRequest(BuyingController):
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
- target_ref_field = 'qty' if self.material_request_type == "Manufacture" else 'stock_qty'
self._update_percent_field({
"target_dt": "Material Request Item",
"target_parent_dt": self.doctype,
"target_parent_field": "per_ordered",
- "target_ref_field": target_ref_field,
+ "target_ref_field": "stock_qty",
"target_field": "ordered_qty",
"name": self.name,
}, update_modified)
@@ -482,7 +481,7 @@ def raise_work_orders(material_request):
default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
for d in mr.items:
- if (d.qty - d.ordered_qty) >0:
+ if (d.stock_qty - d.ordered_qty) > 0:
if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}):
wo_order = frappe.new_doc("Work Order")
wo_order.update({
@@ -514,7 +513,7 @@ def raise_work_orders(material_request):
msgprint(_("The following Work Orders were created:") + '\n' + new_line_sep(message))
if errors:
- frappe.throw(_("Productions Orders cannot be raised for:") + '\n' + new_line_sep(errors))
+ frappe.throw(_("Work Order cannot be created for following reason:") + '\n' + new_line_sep(errors))
return work_orders
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index b925aedd1ac..19924b16363 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -7,7 +7,8 @@
from __future__ import unicode_literals
import frappe, unittest, erpnext
from frappe.utils import flt, today
-from erpnext.stock.doctype.material_request.material_request import raise_work_orders
+from erpnext.stock.doctype.material_request.material_request \
+ import raise_work_orders, make_stock_entry, make_purchase_order, make_supplier_quotation
from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase):
@@ -15,8 +16,6 @@ class TestMaterialRequest(unittest.TestCase):
erpnext.set_perpetual_inventory(0)
def test_make_purchase_order(self):
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
-
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_purchase_order,
@@ -30,8 +29,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(len(po.get("items")), len(mr.get("items")))
def test_make_supplier_quotation(self):
- from erpnext.stock.doctype.material_request.material_request import make_supplier_quotation
-
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name)
@@ -45,12 +42,9 @@ class TestMaterialRequest(unittest.TestCase):
def test_make_stock_entry(self):
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
mr = frappe.copy_doc(test_records[0]).insert()
- self.assertRaises(frappe.ValidationError, make_stock_entry,
- mr.name)
+ self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.material_request_type = "Material Transfer"
@@ -62,40 +56,40 @@ class TestMaterialRequest(unittest.TestCase):
def _insert_stock_entry(self, qty1, qty2, warehouse = None ):
se = frappe.get_doc({
- "company": "_Test Company",
- "doctype": "Stock Entry",
- "posting_date": "2013-03-01",
- "posting_time": "00:00:00",
- "purpose": "Material Receipt",
- "items": [
- {
- "conversion_factor": 1.0,
- "doctype": "Stock Entry Detail",
- "item_code": "_Test Item Home Desktop 100",
- "parentfield": "items",
- "basic_rate": 100,
- "qty": qty1,
- "stock_uom": "_Test UOM 1",
- "transfer_qty": qty1,
- "uom": "_Test UOM 1",
- "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": "_Test Cost Center - _TC"
- },
- {
- "conversion_factor": 1.0,
- "doctype": "Stock Entry Detail",
- "item_code": "_Test Item Home Desktop 200",
- "parentfield": "items",
- "basic_rate": 100,
- "qty": qty2,
- "stock_uom": "_Test UOM 1",
- "transfer_qty": qty2,
- "uom": "_Test UOM 1",
- "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": "_Test Cost Center - _TC"
- }
- ]
- })
+ "company": "_Test Company",
+ "doctype": "Stock Entry",
+ "posting_date": "2013-03-01",
+ "posting_time": "00:00:00",
+ "purpose": "Material Receipt",
+ "items": [
+ {
+ "conversion_factor": 1.0,
+ "doctype": "Stock Entry Detail",
+ "item_code": "_Test Item Home Desktop 100",
+ "parentfield": "items",
+ "basic_rate": 100,
+ "qty": qty1,
+ "stock_uom": "_Test UOM 1",
+ "transfer_qty": qty1,
+ "uom": "_Test UOM 1",
+ "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ },
+ {
+ "conversion_factor": 1.0,
+ "doctype": "Stock Entry Detail",
+ "item_code": "_Test Item Home Desktop 200",
+ "parentfield": "items",
+ "basic_rate": 100,
+ "qty": qty2,
+ "stock_uom": "_Test UOM 1",
+ "transfer_qty": qty2,
+ "uom": "_Test UOM 1",
+ "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ }
+ ]
+ })
se.set_stock_entry_type()
se.insert()
@@ -198,14 +192,7 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # check if per complete is None
- mr.load_from_db()
- self.assertEqual(mr.per_ordered, 0)
- self.assertEqual(mr.get("items")[0].ordered_qty, 0)
- self.assertEqual(mr.get("items")[1].ordered_qty, 0)
-
# map a purchase order
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
po_doc = make_purchase_order(mr.name)
po_doc.supplier = "_Test Supplier"
po_doc.transaction_date = "2013-07-07"
@@ -276,10 +263,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0)
-
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
# map a stock entry
se_doc = make_stock_entry(mr.name)
@@ -331,8 +316,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 27.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 1.5)
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
# check if per complete is as expected for Stock Entry cancelled
se.cancel()
@@ -344,8 +329,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0)
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_completed_qty_for_over_transfer(self):
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
@@ -357,14 +342,7 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # check if per complete is None
- mr.load_from_db()
- self.assertEqual(mr.per_ordered, 0)
- self.assertEqual(mr.get("items")[0].ordered_qty, 0)
- self.assertEqual(mr.get("items")[1].ordered_qty, 0)
-
# map a stock entry
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name)
se_doc.update({
@@ -425,8 +403,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
- self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0)
- self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0)
+ self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
+ self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_incorrect_mapping_of_stock_entry(self):
# submit material request of type Transfer
@@ -435,9 +413,6 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # map a stock entry
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
se_doc = make_stock_entry(mr.name)
se_doc.update({
"posting_date": "2013-03-01",
@@ -468,8 +443,6 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert()
mr.submit()
- # map a stock entry
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name)
self.assertEqual(se_doc.get("items")[0].s_warehouse, "_Test Warehouse - _TC")
@@ -483,8 +456,6 @@ class TestMaterialRequest(unittest.TestCase):
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty"))
def test_make_stock_entry_for_material_issue(self):
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_stock_entry,
@@ -503,8 +474,6 @@ class TestMaterialRequest(unittest.TestCase):
return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100",
"warehouse": "_Test Warehouse - _TC"}, "indented_qty"))
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
-
existing_requested_qty = _get_requested_qty()
mr = frappe.copy_doc(test_records[0])
@@ -563,9 +532,37 @@ class TestMaterialRequest(unittest.TestCase):
item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0]
self.assertEqual(requested_qty, new_requested_qty)
- def test_multi_uom_for_purchase(self):
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+ def test_requested_qty_multi_uom(self):
+ existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
+ uom="_Test UOM 1", conversion_factor=12)
+
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+
+ self.assertEqual(requested_qty, existing_requested_qty + 120)
+
+ work_order = raise_work_orders(mr.name)
+ wo = frappe.get_doc("Work Order", work_order[0])
+ wo.qty = 50
+ wo.wip_warehouse = "_Test Warehouse 1 - _TC"
+ wo.submit()
+
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ self.assertEqual(requested_qty, existing_requested_qty + 70)
+
+ wo.cancel()
+
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ self.assertEqual(requested_qty, existing_requested_qty + 120)
+
+ mr.reload()
+ mr.cancel()
+ requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ self.assertEqual(requested_qty, existing_requested_qty)
+
+
+ def test_multi_uom_for_purchase(self):
mr = frappe.copy_doc(test_records[0])
mr.material_request_type = 'Purchase'
item = mr.items[0]
@@ -607,7 +604,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(mr.per_ordered, 100)
def test_customer_provided_parts_mr(self):
- from erpnext.stock.doctype.material_request.material_request import make_stock_entry
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
@@ -633,6 +629,8 @@ def make_material_request(**args):
mr.append("items", {
"item_code": args.item_code or "_Test Item",
"qty": args.qty or 10,
+ "uom": args.uom or "_Test UOM",
+ "conversion_factor": args.conversion_factor or 1,
"schedule_date": args.schedule_date or today(),
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC"
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index 30206b62d0c..56049131bb4 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -1,5 +1,4 @@
{
- "actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:28:02",
"doctype": "DocType",
@@ -374,7 +373,10 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
- "label": "Received Quantity"
+ "label": "Received Quantity",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
},
{
"collapsible": 1,
@@ -410,7 +412,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-07 18:37:54.495112",
+ "modified": "2020-04-16 09:00:00.992835",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 278971125f0..d46b98b461b 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -38,13 +38,17 @@ frappe.ui.form.on('Pick List', {
};
});
},
- get_item_locations: (frm) => {
- if (!frm.doc.locations || !frm.doc.locations.length) {
- frappe.msgprint(__('First add items in the Item Locations table'));
+ set_item_locations:(frm, save) => {
+ if (!(frm.doc.locations && frm.doc.locations.length)) {
+ frappe.msgprint(__('Add items in the Item Locations table'));
} else {
- frm.call('set_item_locations');
+ frm.call('set_item_locations', {save: save});
}
},
+ get_item_locations: (frm) => {
+ // Button on the form
+ frm.events.set_item_locations(frm, false);
+ },
refresh: (frm) => {
frm.trigger('add_get_items_button');
if (frm.doc.docstatus === 1) {
@@ -52,8 +56,13 @@ frappe.ui.form.on('Pick List', {
'pick_list_name': frm.doc.name,
'purpose': frm.doc.purpose
}).then(target_document_exists => {
+ frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
+
if (target_document_exists) return;
- if (frm.doc.purpose === 'Delivery against Sales Order') {
+
+ frm.add_custom_button(__('Update Current Stock'), () => frm.trigger('update_pick_list_stock'));
+
+ if (frm.doc.purpose === 'Delivery') {
frm.add_custom_button(__('Delivery Note'), () => frm.trigger('create_delivery_note'), __('Create'));
} else {
frm.add_custom_button(__('Stock Entry'), () => frm.trigger('create_stock_entry'), __('Create'));
@@ -105,6 +114,7 @@ frappe.ui.form.on('Pick List', {
method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note',
frm: frm
});
+
},
create_stock_entry: (frm) => {
frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', {
@@ -114,9 +124,12 @@ frappe.ui.form.on('Pick List', {
frappe.set_route("Form", 'Stock Entry', stock_entry.name);
});
},
+ update_pick_list_stock: (frm) => {
+ frm.events.set_item_locations(frm, true);
+ },
add_get_items_button: (frm) => {
let purpose = frm.doc.purpose;
- if (purpose != 'Delivery against Sales Order' || frm.doc.docstatus !== 0) return;
+ if (purpose != 'Delivery' || frm.doc.docstatus !== 0) return;
let get_query_filters = {
docstatus: 1,
per_delivered: ['<', 100],
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index 8d5ef3d12ab..c01388dcd21 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2019-07-11 16:03:13.681045",
"doctype": "DocType",
@@ -44,7 +45,7 @@
"options": "Warehouse"
},
{
- "depends_on": "eval:doc.purpose==='Delivery against Sales Order'",
+ "depends_on": "eval:doc.purpose==='Delivery'",
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
@@ -59,6 +60,7 @@
"options": "Work Order"
},
{
+ "allow_on_submit": 1,
"fieldname": "locations",
"fieldtype": "Table",
"label": "Item Locations",
@@ -86,7 +88,7 @@
"fieldname": "purpose",
"fieldtype": "Select",
"label": "Purpose",
- "options": "Material Transfer for Manufacture\nMaterial Transfer\nDelivery against Sales Order"
+ "options": "Material Transfer for Manufacture\nMaterial Transfer\nDelivery"
},
{
"depends_on": "eval:['Material Transfer', 'Material Issue'].includes(doc.purpose)",
@@ -111,7 +113,8 @@
}
],
"is_submittable": 1,
- "modified": "2019-08-29 21:10:11.572387",
+ "links": [],
+ "modified": "2020-03-17 11:38:41.932875",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 7ed998f979a..917524929e1 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -29,7 +29,7 @@ class PickList(Document):
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)))
- def set_item_locations(self):
+ def set_item_locations(self, save=False):
items = self.aggregate_item_qty()
self.item_location_map = frappe._dict()
@@ -43,7 +43,7 @@ class PickList(Document):
item_code = item_doc.item_code
self.item_location_map.setdefault(item_code,
- get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code)))
+ get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company))
locations = get_items_with_location_and_quantity(item_doc, self.item_location_map)
@@ -59,12 +59,17 @@ class PickList(Document):
location.update(row)
self.append('locations', location)
+ if save:
+ self.save()
+
def aggregate_item_qty(self):
locations = self.get('locations')
self.item_count_map = {}
# aggregate qty for same item
item_map = OrderedDict()
for item in locations:
+ if not item.item_code:
+ frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, reference)
@@ -85,6 +90,10 @@ class PickList(Document):
return item_map.values()
+def validate_item_locations(pick_list):
+ if not pick_list.locations:
+ frappe.throw(_("Add items in the Item Locations table"))
+
def get_items_with_location_and_quantity(item_doc, item_location_map):
available_locations = item_location_map.get(item_doc.item_code)
locations = []
@@ -130,14 +139,14 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
item_location_map[item_doc.item_code] = available_locations
return locations
-def get_available_item_locations(item_code, from_warehouses, required_qty):
+def get_available_item_locations(item_code, from_warehouses, required_qty, company):
locations = []
if frappe.get_cached_value('Item', item_code, 'has_serial_no'):
- locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty)
+ locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company)
elif frappe.get_cached_value('Item', item_code, 'has_batch_no'):
- locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty)
+ locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company)
else:
- locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty)
+ locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company)
total_qty_available = sum(location.get('qty') for location in locations)
@@ -150,9 +159,10 @@ def get_available_item_locations(item_code, from_warehouses, required_qty):
return locations
-def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty):
+def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company):
filters = frappe._dict({
'item_code': item_code,
+ 'company': company,
'warehouse': ['!=', '']
})
@@ -180,7 +190,7 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses,
return locations
-def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty):
+def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company):
warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else ''
batch_locations = frappe.db.sql("""
SELECT
@@ -192,6 +202,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
WHERE
sle.batch_no = batch.name
and sle.`item_code`=%(item_code)s
+ and sle.`company` = %(company)s
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition}
GROUP BY
@@ -202,16 +213,20 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`
""".format(warehouse_condition=warehouse_condition), { #nosec
'item_code': item_code,
+ 'company': company,
'today': today(),
'warehouses': from_warehouses
}, as_dict=1)
return batch_locations
-def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty):
+def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company):
# gets all items available in different warehouses
+ warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")]
+
filters = frappe._dict({
'item_code': item_code,
+ 'warehouse': ['in', warehouses],
'actual_qty': ['>', 0]
})
@@ -230,7 +245,9 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ
@frappe.whitelist()
def create_delivery_note(source_name, target_doc=None):
pick_list = frappe.get_doc('Pick List', source_name)
- sales_orders = [d.sales_order for d in pick_list.locations]
+ validate_item_locations(pick_list)
+
+ sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
sales_orders = set(sales_orders)
delivery_note = None
@@ -238,6 +255,10 @@ def create_delivery_note(source_name, target_doc=None):
delivery_note = create_delivery_note_from_sales_order(sales_order,
delivery_note, skip_item_mapping=True)
+ # map rows without sales orders as well
+ if not delivery_note:
+ delivery_note = frappe.new_doc("Delivery Note")
+
item_table_mapper = {
'doctype': 'Delivery Note Item',
'field_map': {
@@ -248,9 +269,25 @@ def create_delivery_note(source_name, target_doc=None):
'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
}
+ item_table_mapper_without_so = {
+ 'doctype': 'Delivery Note Item',
+ 'field_map': {
+ 'rate': 'rate',
+ 'name': 'name',
+ 'parent': '',
+ }
+ }
+
for location in pick_list.locations:
- sales_order_item = frappe.get_cached_doc('Sales Order Item', location.sales_order_item)
- dn_item = map_child_doc(sales_order_item, delivery_note, item_table_mapper)
+ if location.sales_order_item:
+ sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
+ else:
+ sales_order_item = None
+
+ source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
+ else [location, item_table_mapper_without_so]
+
+ dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
if dn_item:
dn_item.warehouse = location.warehouse
@@ -258,7 +295,7 @@ def create_delivery_note(source_name, target_doc=None):
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
- update_delivery_note_item(sales_order_item, dn_item, delivery_note)
+ update_delivery_note_item(source_doc, dn_item, delivery_note)
set_delivery_note_missing_values(delivery_note)
@@ -269,6 +306,7 @@ def create_delivery_note(source_name, target_doc=None):
@frappe.whitelist()
def create_stock_entry(pick_list):
pick_list = frappe.get_doc(json.loads(pick_list))
+ validate_item_locations(pick_list)
if stock_entry_exists(pick_list.get('name')):
return frappe.msgprint(_('Stock Entry has been already created against this Pick List'))
@@ -318,7 +356,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
@frappe.whitelist()
def target_document_exists(pick_list_name, purpose):
- if purpose == 'Delivery against Sales Order':
+ if purpose == 'Delivery':
return frappe.db.exists('Delivery Note', {
'pick_list': pick_list_name
})
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index c7a35df51f5..71fbf9a866a 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-07-11 16:01:22.832885",
"doctype": "DocType",
"editable_grid": 1,
@@ -8,6 +9,7 @@
"item_name",
"column_break_2",
"description",
+ "item_group",
"section_break_5",
"warehouse",
"quantity_section",
@@ -120,7 +122,8 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
- "options": "Item"
+ "options": "Item",
+ "reqd": 1
},
{
"fieldname": "quantity_section",
@@ -166,10 +169,18 @@
"fieldtype": "Data",
"label": "Material Request Item",
"read_only": 1
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Data",
+ "label": "Item Group",
+ "read_only": 1
}
],
"istable": 1,
- "modified": "2019-08-29 21:28:39.539007",
+ "links": [],
+ "modified": "2020-03-13 19:08:21.995986",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 00a50a88ef0..889d7326e90 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -59,9 +59,9 @@
"base_total",
"base_net_total",
"column_break_27",
+ "total_net_weight",
"total",
"net_total",
- "total_net_weight",
"taxes_charges_section",
"tax_category",
"shipping_col",
@@ -1065,7 +1065,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2020-04-06 16:31:37.444891",
+ "modified": "2020-04-17 13:06:26.970288",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index a51b25bf36d..09271cebfa8 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -355,8 +355,8 @@ class TestPurchaseReceipt(unittest.TestCase):
'accounts': [{
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
- 'accumulated_depreciation_account': 'Depreciation - _TC',
- 'depreciation_expense_account': 'Depreciation - _TC'
+ 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
+ 'depreciation_expense_account': '_Test Depreciation - _TC'
}]
}).insert()
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py
new file mode 100644
index 00000000000..db459575f3e
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py
@@ -0,0 +1,12 @@
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'quality_inspection_template',
+ 'transactions': [
+ {
+ 'label': _('Quality Inspection'),
+ 'items': ['Quality Inspection']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index b54233f4585..529602f2b9c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -220,8 +220,8 @@ frappe.ui.form.on('Stock Entry', {
},
get_query_filters: {
docstatus: 1,
- material_request_type: "Material Transfer",
- status: ['!=', 'Transferred']
+ material_request_type: ["in", ["Material Transfer", "Material Issue"]],
+ status: ["not in", ["Transferred", "Issued"]]
}
})
}, __("Get items from"));
@@ -305,10 +305,9 @@ frappe.ui.form.on('Stock Entry', {
callback: function(r) {
if (!r.exe && r.message){
frappe.model.set_value(cdt, cdn, "serial_no", r.message);
-
- if (callback) {
- callback();
- }
+ }
+ if (callback) {
+ callback();
}
}
});
@@ -425,9 +424,10 @@ frappe.ui.form.on('Stock Entry', {
item.amount = flt(item.basic_amount + flt(item.additional_cost),
precision("amount", item));
- item.valuation_rate = flt(flt(item.basic_rate)
- + (flt(item.additional_cost) / flt(item.transfer_qty)),
- precision("valuation_rate", item));
+ if (flt(item.transfer_qty)) {
+ item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
+ precision("valuation_rate", item));
+ }
}
refresh_field('items');
@@ -799,39 +799,17 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
},
- source_mandatory: ["Material Issue", "Material Transfer", "Send to Subcontractor",
- "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"],
- target_mandatory: ["Material Receipt", "Material Transfer", "Send to Subcontractor",
- "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"],
-
from_warehouse: function(doc) {
- var me = this;
- this.set_warehouse_if_different("s_warehouse", doc.from_warehouse, function(row) {
- return me.source_mandatory.indexOf(me.frm.doc.purpose)!==-1;
- });
+ this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse);
},
to_warehouse: function(doc) {
- var me = this;
- this.set_warehouse_if_different("t_warehouse", doc.to_warehouse, function(row) {
- return me.target_mandatory.indexOf(me.frm.doc.purpose)!==-1;
- });
+ this.set_warehouse_in_children(doc.items, "t_warehouse", doc.to_warehouse);
},
- set_warehouse_if_different: function(fieldname, value, condition) {
- var changed = false;
- for (var i=0, l=(this.frm.doc.items || []).length; i" + _("An error occured for certain Items while creating Material Requests based on Re-order level. \
+ Please rectify these issues :") + "
"
-An error occured for certain Items while creating Material Requests based on Re-order level.
+ for exception in exceptions_list:
+ exception = json.loads(exception)
+ error_message = """{0}
""".format(_(exception.get("message")))
+ content += error_message
-Please rectify these issues:
----
-
-%s
-
----
-Regards,
-Administrator""" % ("\n\n".join(exceptions_list),)
+ content += _("Regards,") + "
" + _("Administrator")
from frappe.email import sendmail_to_system_managers
sendmail_to_system_managers(subject, content)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js
index 537fa7c04b8..7d22823eb80 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.js
+++ b/erpnext/stock/report/stock_balance/stock_balance.js
@@ -3,6 +3,14 @@
frappe.query_reports["Stock Balance"] = {
"filters": [
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "width": "80",
+ "options": "Company",
+ "default": frappe.defaults.get_default("company")
+ },
{
"fieldname":"from_date",
"label": __("From Date"),
@@ -26,12 +34,6 @@ frappe.query_reports["Stock Balance"] = {
"width": "80",
"options": "Item Group"
},
- {
- "fieldname":"brand",
- "label": __("Brand"),
- "fieldtype": "Link",
- "options": "Brand"
- },
{
"fieldname": "item_code",
"label": __("Item"),
@@ -84,5 +86,18 @@ frappe.query_reports["Stock Balance"] = {
"label": __('Show Stock Ageing Data'),
"fieldtype": 'Check'
},
- ]
+ ],
+
+ "formatter": function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "out_qty" && data && data.out_qty > 0) {
+ value = "" + value + "";
+ }
+ else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
+ value = "" + value + "";
+ }
+
+ return value;
+ }
};
diff --git a/erpnext/stock/report/stock_balance/stock_balance.json b/erpnext/stock/report/stock_balance/stock_balance.json
index 2f20b202350..8c45f0c2f47 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.json
+++ b/erpnext/stock/report/stock_balance/stock_balance.json
@@ -1,24 +1,26 @@
{
- "add_total_row": 1,
- "creation": "2014-10-10 17:58:11.577901",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2018-08-14 15:24:41.395557",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Stock Balance",
- "owner": "Administrator",
- "prepared_report": 1,
- "ref_doctype": "Stock Ledger Entry",
- "report_name": "Stock Balance",
- "report_type": "Script Report",
+ "add_total_row": 1,
+ "creation": "2014-10-10 17:58:11.577901",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 2,
+ "is_standard": "Yes",
+ "modified": "2020-04-30 13:46:14.680354",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Balance",
+ "owner": "Administrator",
+ "prepared_report": 1,
+ "query": "",
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Stock Balance",
+ "report_type": "Script Report",
"roles": [
{
"role": "Stock User"
- },
+ },
{
"role": "Accounts Manager"
}
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index ff03381389c..74a4f6ef142 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -94,8 +94,6 @@ def get_columns(filters):
{"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
{"label": _("Item Name"), "fieldname": "item_name", "width": 150},
{"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
- {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 90},
- {"label": _("Description"), "fieldname": "description", "width": 140},
{"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100},
{"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90},
{"label": _("Balance Qty"), "fieldname": "bal_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"},
@@ -132,6 +130,9 @@ def get_conditions(filters):
else:
frappe.throw(_("'To Date' is required"))
+ if filters.get("company"):
+ conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company"))
+
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse",
filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
@@ -170,6 +171,8 @@ def get_item_warehouse_map(filters, sle):
from_date = getdate(filters.get("from_date"))
to_date = getdate(filters.get("to_date"))
+ float_precision = cint(frappe.db.get_default("float_precision")) or 3
+
for d in sle:
key = (d.company, d.item_code, d.warehouse)
if key not in iwb_map:
@@ -184,7 +187,7 @@ def get_item_warehouse_map(filters, sle):
qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)]
if d.voucher_type == "Stock Reconciliation":
- qty_diff = flt(d.qty_after_transaction) - qty_dict.bal_qty
+ qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty)
else:
qty_diff = flt(d.actual_qty)
@@ -195,7 +198,7 @@ def get_item_warehouse_map(filters, sle):
qty_dict.opening_val += value_diff
elif d.posting_date >= from_date and d.posting_date <= to_date:
- if qty_diff > 0:
+ if flt(qty_diff, float_precision) >= 0:
qty_dict.in_qty += qty_diff
qty_dict.in_val += value_diff
else:
@@ -206,16 +209,15 @@ def get_item_warehouse_map(filters, sle):
qty_dict.bal_qty += qty_diff
qty_dict.bal_val += value_diff
- iwb_map = filter_items_with_no_transactions(iwb_map)
+ iwb_map = filter_items_with_no_transactions(iwb_map, float_precision)
return iwb_map
-def filter_items_with_no_transactions(iwb_map):
+def filter_items_with_no_transactions(iwb_map, float_precision):
for (company, item, warehouse) in sorted(iwb_map):
qty_dict = iwb_map[(company, item, warehouse)]
no_transactions = True
- float_precision = cint(frappe.db.get_default("float_precision")) or 3
for key, val in iteritems(qty_dict):
val = flt(val, float_precision)
qty_dict[key] = val
@@ -232,8 +234,6 @@ def get_items(filters):
if filters.get("item_code"):
conditions.append("item.name=%(item_code)s")
else:
- if filters.get("brand"):
- conditions.append("item.brand=%(brand)s")
if filters.get("item_group"):
conditions.append(get_item_group_condition(filters.get("item_group")))
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 2bdb04ed2c9..56973153609 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -113,24 +113,30 @@ def get_reserved_qty(item_code, warehouse):
return flt(reserved_qty[0][0]) if reserved_qty else 0
def get_indented_qty(item_code, warehouse):
- inward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor)
- from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
- where mr_item.item_code=%s and mr_item.warehouse=%s
- and mr.material_request_type in ('Purchase', 'Manufacture')
- and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name
- and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
-
- outward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor)
+ # Ordered Qty is always maintained in stock UOM
+ inward_qty = frappe.db.sql("""
+ select sum(mr_item.stock_qty - mr_item.ordered_qty)
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
where mr_item.item_code=%s and mr_item.warehouse=%s
- and mr.material_request_type in ('Material Issue', 'Material Transfer')
- and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name
- and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
+ and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer')
+ and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
+ and mr.status!='Stopped' and mr.docstatus=1
+ """, (item_code, warehouse))
+ inward_qty = flt(inward_qty[0][0]) if inward_qty else 0
- inward_qty, outward_qty = flt(inward_qty[0][0]) if inward_qty else 0, flt(outward_qty[0][0]) if outward_qty else 0
- indented_qty = inward_qty - outward_qty
+ outward_qty = frappe.db.sql("""
+ select sum(mr_item.stock_qty - mr_item.ordered_qty)
+ from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
+ where mr_item.item_code=%s and mr_item.warehouse=%s
+ and mr.material_request_type = 'Material Issue'
+ and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
+ and mr.status!='Stopped' and mr.docstatus=1
+ """, (item_code, warehouse))
+ outward_qty = flt(outward_qty[0][0]) if outward_qty else 0
- return indented_qty
+ requested_qty = inward_qty - outward_qty
+
+ return requested_qty
def get_ordered_qty(item_code, warehouse):
ordered_qty = frappe.db.sql("""
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index b748e3fa46e..11fbc82a356 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -338,8 +338,13 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
ignore_permissions = False
if is_website_user():
- if not filters: filters = []
- filters.append(("Issue", "customer", "=", customer)) if customer else filters.append(("Issue", "raised_by", "=", user))
+ if not filters: filters = {}
+
+ if customer:
+ filters["customer"] = customer
+ else:
+ filters["raised_by"] = user
+
ignore_permissions = True
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)
diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html
index c27e4cede0d..c2f13539cdf 100644
--- a/erpnext/templates/includes/itemised_tax_breakup.html
+++ b/erpnext/templates/includes/itemised_tax_breakup.html
@@ -16,7 +16,11 @@
| {{ item }} |
- {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, currency) }}
+ {% if doc.get('is_return') %}
+ {{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }}
+ {% else %}
+ {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, doc.currency) }}
+ {% endif %}
|
{% for tax_account in tax_accounts %}
{% set tax_details = taxes.get(tax_account) %}
@@ -25,7 +29,11 @@
{% if tax_details.tax_rate or not tax_details.tax_amount %}
({{ tax_details.tax_rate }}%)
{% endif %}
- {{ frappe.utils.fmt_money(tax_details.tax_amount / conversion_rate, None, currency) }}
+ {% if doc.get('is_return') %}
+ {{ frappe.utils.fmt_money((tax_details.tax_amount / doc.conversion_rate)|abs, None, doc.currency) }}
+ {% else %}
+ {{ frappe.utils.fmt_money(tax_details.tax_amount / doc.conversion_rate, None, doc.currency) }}
+ {% endif %}
{% else %}
|
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 20998108467..ef7bd989042 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import frappe.share
from frappe import _
-from frappe.utils import cstr, now_datetime, cint, flt, get_time
+from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_link_to_form
from erpnext.controllers.status_updater import StatusUpdater
from six import string_types
@@ -123,8 +123,11 @@ class TransactionBase(StatusUpdater):
ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= .01:
- frappe.throw(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ")
+ frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ")
.format(d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate))
+ frappe.throw(_("To allow different rates, disable the {0} checkbox in {1}.")
+ .format(frappe.bold("Maintain Same Rate Throughout Sales Cycle"),
+ get_link_to_form("Selling Settings", "Selling Settings", frappe.bold("Selling Settings"))))
def get_link_filters(self, for_doctype):
if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):
@@ -176,4 +179,6 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
qty = d.get(f)
if qty:
if abs(cint(qty) - flt(qty)) > 0.0000001:
- frappe.throw(_("Quantity ({0}) cannot be a fraction in row {1}").format(qty, d.idx), UOMMustBeIntegerError)
+ frappe.throw(_("Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.") \
+ .format(qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))),
+ UOMMustBeIntegerError)