diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 9b972e7561e..54ffe3f6a14 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '12.8.0'
+__version__ = '12.9.0'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 62a8f05c659..cbb151f95a3 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -159,7 +159,7 @@ def book_deferred_income_or_expense(doc, posting_date=None):
total_days, total_booking_days, account_currency)
make_gl_entries(doc, credit_account, debit_account, against,
- amount, base_amount, end_date, project, account_currency, item.cost_center, item.name)
+ amount, base_amount, end_date, project, account_currency, item.cost_center, item)
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
_book_deferred_revenue_or_expense(item)
@@ -170,7 +170,7 @@ def book_deferred_income_or_expense(doc, posting_date=None):
_book_deferred_revenue_or_expense(item)
def make_gl_entries(doc, credit_account, debit_account, against,
- amount, base_amount, posting_date, project, account_currency, cost_center, voucher_detail_no):
+ amount, base_amount, posting_date, project, account_currency, cost_center, item):
# GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries
@@ -184,10 +184,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
"credit": base_amount,
"credit_in_account_currency": amount,
"cost_center": cost_center,
- "voucher_detail_no": voucher_detail_no,
+ "voucher_detail_no": item.name,
'posting_date': posting_date,
'project': project
- }, account_currency)
+ }, account_currency, item=item)
)
# GL Entry to debit the amount from the expense
gl_entries.append(
@@ -197,10 +197,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
"debit": base_amount,
"debit_in_account_currency": amount,
"cost_center": cost_center,
- "voucher_detail_no": voucher_detail_no,
+ "voucher_detail_no": item.name,
'posting_date': posting_date,
'project': project
- }, account_currency)
+ }, account_currency, item=item)
)
if gl_entries:
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..894ec5bdec5 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -162,9 +162,9 @@ def toggle_disabling(doc):
def get_doctypes_with_dimensions():
doclist = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
- "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Sales Invoice Item", "Purchase Invoice Item",
- "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item",
- "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
+ "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
+ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
+ "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan"]
@@ -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/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
index 40a97ae295e..0b7cff3d63c 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
@@ -17,17 +17,60 @@ frappe.ui.form.on('Chart of Accounts Importer', {
if (frm.page && frm.page.show_import_button) {
create_import_button(frm);
}
+ },
- // show download template button when company is properly selected
- if(frm.doc.company) {
- // download the csv template file
- frm.add_custom_button(__("Download template"), function () {
- let get_template_url = 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template';
- open_url_post(frappe.request.url, { cmd: get_template_url, doctype: frm.doc.doctype });
- });
- } else {
- frm.set_value("import_file", "");
- }
+ download_template: function(frm) {
+ var d = new frappe.ui.Dialog({
+ title: __("Download Template"),
+ fields: [
+ {
+ label : "File Type",
+ fieldname: "file_type",
+ fieldtype: "Select",
+ reqd: 1,
+ options: ["Excel", "CSV"]
+ },
+ {
+ label: "Template Type",
+ fieldname: "template_type",
+ fieldtype: "Select",
+ reqd: 1,
+ options: ["Sample Template", "Blank Template"],
+ change: () => {
+ let template_type = d.get_value('template_type');
+
+ if (template_type === "Sample Template") {
+ d.set_df_property('template_type', 'description',
+ `The Sample Template contains all the required accounts pre filled in the template.
+ You can add more accounts or change existing accounts in the template as per your choice.`);
+ } else {
+ d.set_df_property('template_type', 'description',
+ `The Blank Template contains just the account type and root type required to build the Chart
+ of Accounts. Please enter the account names and add more rows as per your requirement.`);
+ }
+ }
+ }
+ ],
+ primary_action: function() {
+ var data = d.get_values();
+
+ if (!data.template_type) {
+ frappe.throw(__('Please select Template Type to download template'));
+ }
+
+ open_url_post(
+ '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template',
+ {
+ file_type: data.file_type,
+ template_type: data.template_type
+ }
+ );
+
+ d.hide();
+ },
+ primary_action_label: __('Download')
+ });
+ d.show();
},
import_file: function (frm) {
@@ -41,21 +84,24 @@ frappe.ui.form.on('Chart of Accounts Importer', {
},
company: function (frm) {
- // validate that no Gl Entry record for the company exists.
- frappe.call({
- method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company",
- args: {
- company: frm.doc.company
- },
- callback: function(r) {
- if(r.message===false) {
- frm.set_value("company", "");
- frappe.throw(__("Transactions against the company already exist! "));
- } else {
- frm.trigger("refresh");
+ if (frm.doc.company) {
+ // validate that no Gl Entry record for the company exists.
+ frappe.call({
+ method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company",
+ args: {
+ company: frm.doc.company
+ },
+ callback: function(r) {
+ if(r.message===false) {
+ frm.set_value("company", "");
+ frappe.throw(__(`Transactions against the company already exist!
+ Chart Of accounts can be imported for company with no transactions`));
+ } else {
+ frm.trigger("refresh");
+ }
}
- }
- });
+ });
+ }
}
});
@@ -77,7 +123,7 @@ var validate_csv_data = function(frm) {
};
var create_import_button = function(frm) {
- frm.page.set_primary_action(__("Start Import"), function () {
+ frm.page.set_primary_action(__("Import"), function () {
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
args: {
@@ -118,7 +164,8 @@ var generate_tree_preview = function(frm) {
args: {
file_name: frm.doc.import_file,
parent: parent,
- doctype: 'Chart of Accounts Importer'
+ doctype: 'Chart of Accounts Importer',
+ file_type: frm.doc.file_type
},
onclick: function(node) {
parent = node.value;
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json
index d544e69231e..ee095ac386a 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json
@@ -1,226 +1,71 @@
{
- "allow_copy": 1,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2019-02-01 12:24:34.761380",
- "custom": 0,
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2019-02-01 12:24:34.761380",
"description": "Import Chart of Accounts from a csv file",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Other",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "doctype": "DocType",
+ "document_type": "Other",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "download_template",
+ "import_file",
+ "chart_preview",
+ "chart_tree"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "import_file_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,
- "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,
- "depends_on": "",
+ "depends_on": "company",
"fieldname": "import_file",
"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": "Attach custom Chart of Accounts file",
- "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
- },
+ "label": "Attach custom Chart of Accounts file"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
"collapsible": 1,
- "columns": 0,
"fieldname": "chart_preview",
- "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": "Chart Preview",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldtype": "Section Break",
+ "label": "Chart Preview"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "chart_tree",
"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": "Chart Tree",
- "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": "Chart Tree"
+ },
+ {
+ "depends_on": "company",
+ "fieldname": "download_template",
+ "fieldtype": "Button",
+ "label": "Download Template"
}
- ],
- "has_web_view": 0,
- "hide_heading": 1,
- "hide_toolbar": 1,
- "idx": 0,
- "image_view": 0,
- "in_create": 1,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-02-04 23:10:30.136807",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Chart of Accounts Importer",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "hide_toolbar": 1,
+ "in_create": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-02-28 08:49:11.422846",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Chart of Accounts Importer",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 1,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "quick_entry": 1,
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index f496280317a..4100699fe39 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -4,18 +4,28 @@
from __future__ import unicode_literals
from functools import reduce
-import frappe, csv
+import frappe, csv, os
from frappe import _
-from frappe.utils import cstr
+from frappe.utils import cstr, cint
from frappe.model.document import Document
from frappe.utils.csvutils import UnicodeWriter
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts, build_tree_from_json
+from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document):
pass
@frappe.whitelist()
def validate_company(company):
+ parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company',
+ {'name': company}, ['parent_company',
+ 'allow_account_creation_against_child_company'])
+
+ if parent_company and (not allow_account_creation_against_child_company):
+ frappe.throw(_("""{0} is a child company. Please import accounts against parent company
+ or enable {1} in company master""").format(frappe.bold(company),
+ frappe.bold('Allow Account Creation Against Child Company')), title='Wrong Company')
+
if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
return False
@@ -25,42 +35,85 @@ def import_coa(file_name, company):
unset_existing_data(company)
# create accounts
- forest = build_forest(generate_data_from_csv(file_name))
+ file_doc, extension = get_file(file_name)
+
+ if extension == 'csv':
+ data = generate_data_from_csv(file_doc)
+ else:
+ data = generate_data_from_excel(file_doc, extension)
+
+ forest = build_forest(data)
create_charts(company, custom_chart=forest)
# trigger on_update for company to reset default accounts
set_default_accounts(company)
-def generate_data_from_csv(file_name, as_dict=False):
- ''' read csv file and return the generated nested tree '''
- if not file_name.endswith('.csv'):
- frappe.throw("Only CSV files can be used to for importing data. Please check the file format you are trying to upload")
+def get_file(file_name):
+ file_doc = frappe.get_doc("File", {"file_url": file_name})
+ parts = file_doc.get_extension()
+ extension = parts[1]
+ extension = extension.lstrip(".")
+
+ if extension not in ('csv', 'xlsx', 'xls'):
+ frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
+
+ return file_doc, extension
+
+def generate_data_from_csv(file_doc, as_dict=False):
+ ''' read csv file and return the generated nested tree '''
- file_doc = frappe.get_doc('File', {"file_url": file_name})
file_path = file_doc.get_full_path()
data = []
with open(file_path, 'r') as in_file:
csv_reader = list(csv.reader(in_file))
- headers = csv_reader[1][1:]
- del csv_reader[0:2] # delete top row and headers row
+ headers = csv_reader[0]
+ del csv_reader[0] # delete top row and headers row
for row in csv_reader:
if as_dict:
- data.append({frappe.scrub(header): row[index+1] for index, header in enumerate(headers)})
+ data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
- if not row[2]: row[2] = row[1]
- data.append(row[1:])
+ if not row[1]: row[1] = row[0]
+ data.append(row)
# convert csv data
return data
+def generate_data_from_excel(file_doc, extension, as_dict=False):
+ content = file_doc.get_content()
+
+ if extension == "xlsx":
+ rows = read_xlsx_file_from_attached_file(fcontent=content)
+ elif extension == "xls":
+ rows = read_xls_file_from_attached_file(content)
+
+ data = []
+ headers = rows[0]
+ del rows[0]
+
+ for row in rows:
+ if as_dict:
+ data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
+ else:
+ if not row[1]: row[1] = row[0]
+ data.append(row)
+
+ return data
+
@frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None):
''' called by tree view (to fetch node's children) '''
+ file_doc, extension = get_file(file_name)
parent = None if parent==_('All Accounts') else parent
- forest = build_forest(generate_data_from_csv(file_name))
+
+ if extension == 'csv':
+ data = generate_data_from_csv(file_doc)
+ else:
+ data = generate_data_from_excel(file_doc, extension)
+
+ forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
# filter out to show data for the selected node only
@@ -91,15 +144,18 @@ def build_forest(data):
# returns the path of any node in list format
def return_parent(data, child):
+ from frappe import _
+
for row in data:
account_name, parent_account = row[0:2]
if parent_account == account_name == child:
return [parent_account]
elif account_name == child:
parent_account_list = return_parent(data, parent_account)
- if not parent_account_list and parent_account:
- frappe.throw(_("The parent account {0} does not exists")
- .format(parent_account))
+ if not parent_account_list:
+ frappe.throw(_("The parent account {0} does not exists in the uploaded template").format(
+ frappe.bold(parent_account)))
+
return [child] + parent_account_list
charts_map, paths = {}, []
@@ -114,7 +170,7 @@ def build_forest(data):
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
charts_map[account_name] = {}
- if is_group == 1: charts_map[account_name]["is_group"] = is_group
+ if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
if account_type: charts_map[account_name]["account_type"] = account_type
if root_type: charts_map[account_name]["root_type"] = root_type
if account_number: charts_map[account_name]["account_number"] = account_number
@@ -132,24 +188,94 @@ def build_forest(data):
return out
+def build_response_as_excel(writer):
+ filename = frappe.generate_hash("", 10)
+ with open(filename, 'wb') as f:
+ f.write(cstr(writer.getvalue()).encode('utf-8'))
+ f = open(filename)
+ reader = csv.reader(f)
+
+ from frappe.utils.xlsxutils import make_xlsx
+ xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template")
+
+ f.close()
+ os.remove(filename)
+
+ # write out response as a xlsx type
+ frappe.response['filename'] = 'coa_importer_template.xlsx'
+ frappe.response['filecontent'] = xlsx_file.getvalue()
+ frappe.response['type'] = 'binary'
+
@frappe.whitelist()
-def download_template():
+def download_template(file_type, template_type):
data = frappe._dict(frappe.local.form_dict)
+
+ writer = get_template(template_type)
+
+ if file_type == 'CSV':
+ # download csv file
+ frappe.response['result'] = cstr(writer.getvalue())
+ frappe.response['type'] = 'csv'
+ frappe.response['doctype'] = 'Chart of Accounts Importer'
+ else:
+ build_response_as_excel(writer)
+
+def get_template(template_type):
+
fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"]
writer = UnicodeWriter()
+ writer.writerow(fields)
- writer.writerow([_('Chart of Accounts Template')])
- writer.writerow([_("Column Labels : ")] + fields)
- writer.writerow([_("Start entering data from here : ")])
+ if template_type == 'Blank Template':
+ for root_type in get_root_types():
+ writer.writerow(['', '', '', 1, '', root_type])
+
+ for account in get_mandatory_group_accounts():
+ writer.writerow(['', '', '', 1, account, "Asset"])
+
+ for account_type in get_mandatory_account_types():
+ writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')])
+ else:
+ writer = get_sample_template(writer)
+
+ return writer
+
+def get_sample_template(writer):
+ template = [
+ ["Application Of Funds(Assets)", "", "", 1, "", "Asset"],
+ ["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"],
+ ["Equity", "", "", 1, "", "Equity"],
+ ["Expenses", "", "", 1, "", "Expense"],
+ ["Income", "", "", 1, "", "Income"],
+ ["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"],
+ ["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"],
+ ["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"],
+ ["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"],
+ ["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"],
+ ["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"],
+ ["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"],
+ ["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"],
+ ["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"],
+ ["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"],
+ ["Cash", "Cash In Hand", "", 0, "Cash", "Asset"],
+ ["Stores", "Stock Assets", "", 0, "Stock", "Asset"],
+ ]
+
+ for row in template:
+ writer.writerow(row)
+
+ return writer
- # download csv file
- frappe.response['result'] = cstr(writer.getvalue())
- frappe.response['type'] = 'csv'
- frappe.response['doctype'] = data.get('doctype')
@frappe.whitelist()
def validate_accounts(file_name):
- accounts = generate_data_from_csv(file_name, as_dict=True)
+
+ file_doc, extension = get_file(file_name)
+
+ if extension == 'csv':
+ accounts = generate_data_from_csv(file_doc, as_dict=True)
+ else:
+ accounts = generate_data_from_excel(file_doc, extension, as_dict=True)
accounts_dict = {}
for account in accounts:
@@ -174,12 +300,38 @@ def validate_root(accounts):
for account in roots:
if not account.get("root_type") and account.get("account_name"):
error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
- elif account.get("root_type") not in ("Asset", "Liability", "Expense", "Income", "Equity") and account.get("account_name"):
+ elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
if error_messages:
return "
".join(error_messages)
+def get_root_types():
+ return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
+
+def get_report_type(root_type):
+ if root_type in ('Asset', 'Liability', 'Equity'):
+ return 'Balance Sheet'
+ else:
+ return 'Profit and Loss'
+
+def get_mandatory_group_accounts():
+ return ('Bank', 'Cash', 'Stock')
+
+def get_mandatory_account_types():
+ return [
+ {'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'},
+ {'account_type': 'Depreciation', 'root_type': 'Expense'},
+ {'account_type': 'Fixed Asset', 'root_type': 'Asset'},
+ {'account_type': 'Payable', 'root_type': 'Liability'},
+ {'account_type': 'Receivable', 'root_type': 'Asset'},
+ {'account_type': 'Stock Adjustment', 'root_type': 'Expense'},
+ {'account_type': 'Bank', 'root_type': 'Asset'},
+ {'account_type': 'Cash', 'root_type': 'Asset'},
+ {'account_type': 'Stock', 'root_type': 'Asset'}
+ ]
+
+
def validate_account_types(accounts):
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1]
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/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 512e348c8bf..7358a31469b 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -115,8 +115,8 @@ class GLEntry(Document):
from tabAccount where name=%s""", self.account, as_dict=1)[0]
if ret.is_group==1:
- frappe.throw(_("{0} {1}: Account {2} cannot be a Group")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in
+ transactions''').format(self.voucher_type, self.voucher_no, self.account))
if ret.docstatus==2:
frappe.throw(_("{0} {1}: Account {2} is inactive")
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index 39fc203d53f..594b4d4a223 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -8,6 +8,7 @@ from frappe import _
from frappe.utils import flt, getdate, nowdate, add_days
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class InvoiceDiscounting(AccountsController):
def validate(self):
@@ -81,10 +82,15 @@ class InvoiceDiscounting(AccountsController):
def make_gl_entries(self):
company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+
gl_entries = []
+ invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"]
+ accounting_dimensions = get_accounting_dimensions()
+
+ invoice_fields.extend(accounting_dimensions)
+
for d in self.invoices:
- inv = frappe.db.get_value("Sales Invoice", d.sales_invoice,
- ["debit_to", "party_account_currency", "conversion_rate", "cost_center"], as_dict=1)
+ inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1)
if d.outstanding_amount:
outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate,
@@ -102,7 +108,7 @@ class InvoiceDiscounting(AccountsController):
"cost_center": inv.cost_center,
"against_voucher": d.sales_invoice,
"against_voucher_type": "Sales Invoice"
- }, inv.party_account_currency))
+ }, inv.party_account_currency, item=inv))
gl_entries.append(self.get_gl_dict({
"account": self.accounts_receivable_credit,
@@ -115,7 +121,7 @@ class InvoiceDiscounting(AccountsController):
"cost_center": inv.cost_center,
"against_voucher": d.sales_invoice,
"against_voucher_type": "Sales Invoice"
- }, ar_credit_account_currency))
+ }, ar_credit_account_currency, item=inv))
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
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..ab7aa15e3dd 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,9 +82,10 @@ 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()
-
+
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
update_payment_req_status(self, None)
@@ -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'
@@ -397,8 +448,6 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self):
- if self.remarks: return
-
if self.payment_type=="Internal Transfer":
remarks = [_("Amount {0} {1} transferred from {2} to {3}")
.format(self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to)]
@@ -452,7 +501,7 @@ class PaymentEntry(AccountsController):
"against": against_account,
"account_currency": self.party_account_currency,
"cost_center": self.cost_center
- })
+ }, item=self)
dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
@@ -496,7 +545,7 @@ class PaymentEntry(AccountsController):
"credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount,
"cost_center": self.cost_center
- })
+ }, item=self)
)
if self.payment_type in ("Receive", "Internal Transfer"):
gl_entries.append(
@@ -507,7 +556,7 @@ class PaymentEntry(AccountsController):
"debit_in_account_currency": self.received_amount,
"debit": self.base_received_amount,
"cost_center": self.cost_center
- })
+ }, item=self)
)
def add_deductions_gl_entries(self, gl_entries):
@@ -1012,15 +1061,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 +1085,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_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py
index 3f3174a69b6..7ecdc41d034 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order.py
@@ -80,7 +80,7 @@ def make_journal_entry(doc, supplier, mode_of_payment=None):
paid_amt += d.amount
je.append('accounts', {
- 'account': doc.references[0].account,
+ 'account': doc.account,
'credit_in_account_currency': paid_amt
})
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index f2b0d3db7a1..c863ce8e7f7 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -69,7 +69,7 @@ class PaymentRequest(Document):
elif self.payment_request_type == 'Inward':
self.db_set('status', 'Requested')
- send_mail = self.payment_gateway_validation()
+ send_mail = self.payment_gateway_validation() if self.payment_gateway else None
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart") \
@@ -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/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index 58f12162d14..f8d52a78336 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -15,12 +15,12 @@ class TestPOSProfile(unittest.TestCase):
pos_profile = get_pos_profile("_Test Company") or {}
if pos_profile:
doc = frappe.get_doc("POS Profile", pos_profile.get("name"))
- doc.append('item_groups', {'item_group': '_Test Item Group'})
- doc.append('customer_groups', {'customer_group': '_Test Customer Group'})
+ doc.set('item_groups', [{'item_group': '_Test Item Group'}])
+ doc.set('customer_groups', [{'customer_group': '_Test Customer Group'}])
doc.save()
items = get_items_list(doc, doc.company)
customers = get_customers_list(doc)
-
+
products_count = frappe.db.sql(""" select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1)
customers_count = frappe.db.sql(""" select count(name) from tabCustomer where customer_group = '_Test Customer Group'""")
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index fc0d8b16f71..983f1ef85a0 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -103,7 +103,7 @@ class PricingRule(Document):
self.same_item = 1
def validate_max_discount(self):
- if self.rate_or_discount == "Discount Percentage" and self.items:
+ if self.rate_or_discount == "Discount Percentage" and self.get("items"):
for d in self.items:
max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount")
if max_discount and flt(self.discount_percentage) > flt(max_discount):
@@ -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..cb05481df5a 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -4,13 +4,19 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe, copy, json
-from frappe import throw, _
+
+import copy
+import json
+
from six import string_types
-from frappe.utils import flt, cint, get_datetime, get_link_to_form, today
+
+import frappe
from erpnext.setup.doctype.item_group.item_group import get_child_item_groups
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.get_item_details import get_conversion_factor
+from frappe import _, throw
+from frappe.utils import cint, flt, get_datetime, get_link_to_form, getdate, today
+
class MultiplePricingRuleConflict(frappe.ValidationError): pass
@@ -330,9 +336,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:
@@ -502,18 +508,16 @@ def get_pricing_rule_items(pr_doc):
return list(set(apply_on_data))
def validate_coupon_code(coupon_name):
- from frappe.utils import today,getdate
- coupon=frappe.get_doc("Coupon Code",coupon_name)
+ coupon = frappe.get_doc("Coupon Code", coupon_name)
+
if coupon.valid_from:
- if coupon.valid_from > getdate(today()) :
- frappe.throw(_("Sorry,coupon code validity has not started"))
+ if coupon.valid_from > getdate(today()):
+ frappe.throw(_("Sorry, this coupon code's validity has not started"))
elif coupon.valid_upto:
- if coupon.valid_upto < getdate(today()) :
- frappe.throw(_("Sorry,coupon code validity has expired"))
- elif coupon.used>=coupon.maximum_use:
- frappe.throw(_("Sorry,coupon code are exhausted"))
- else:
- return
+ if coupon.valid_upto < getdate(today()):
+ frappe.throw(_("Sorry, this coupon code's validity has expired"))
+ elif coupon.used >= coupon.maximum_use:
+ frappe.throw(_("Sorry, this coupon code is no longer valid"))
def update_coupon_code_count(coupon_name,transaction_type):
coupon=frappe.get_doc("Coupon Code",coupon_name)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 9292b633fc3..3cf4d5994a5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -261,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..58c521f0ffe 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -452,7 +452,7 @@ class PurchaseInvoice(BuyingController):
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
def make_item_gl_entries(self, gl_entries):
@@ -802,7 +802,7 @@ class PurchaseInvoice(BuyingController):
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
gl_entries.append(
@@ -813,7 +813,7 @@ class PurchaseInvoice(BuyingController):
"credit_in_account_currency": self.base_paid_amount \
if bank_account_currency==self.company_currency else self.paid_amount,
"cost_center": self.cost_center
- }, bank_account_currency)
+ }, bank_account_currency, item=self)
)
def make_write_off_gl_entry(self, gl_entries):
@@ -834,7 +834,7 @@ class PurchaseInvoice(BuyingController):
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
gl_entries.append(
self.get_gl_dict({
@@ -844,7 +844,7 @@ class PurchaseInvoice(BuyingController):
"credit_in_account_currency": self.base_write_off_amount \
if write_off_account_currency==self.company_currency else self.write_off_amount,
"cost_center": self.cost_center or self.write_off_cost_center
- })
+ }, item=self)
)
def make_gle_for_rounding_adjustment(self, gl_entries):
@@ -863,8 +863,7 @@ class PurchaseInvoice(BuyingController):
"debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment,
"cost_center": self.cost_center or round_off_cost_center,
- }
- ))
+ }, item=self))
def on_cancel(self):
super(PurchaseInvoice, self).on_cancel()
@@ -959,7 +958,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
@@ -981,6 +980,40 @@ class PurchaseInvoice(BuyingController):
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
+
+ def set_status(self, update=False, status=None, update_modified=True):
+ if self.is_new():
+ if self.get('amended_from'):
+ self.status = 'Draft'
+ return
+
+ precision = self.precision("outstanding_amount")
+ outstanding_amount = flt(self.outstanding_amount, precision)
+ due_date = getdate(self.due_date)
+ nowdate = getdate()
+
+ if not status:
+ if self.docstatus == 2:
+ status = "Cancelled"
+ elif self.docstatus == 1:
+ if outstanding_amount > 0 and due_date < nowdate:
+ self.status = "Overdue"
+ elif outstanding_amount > 0 and due_date >= nowdate:
+ self.status = "Unpaid"
+ #Check if outstanding amount is 0 due to debit note issued against invoice
+ elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
+ self.status = "Debit Note Issued"
+ elif self.is_return == 1:
+ self.status = "Return"
+ elif outstanding_amount<=0:
+ self.status = "Paid"
+ else:
+ self.status = "Submitted"
+ else:
+ self.status = "Draft"
+
+ if update:
+ self.db_set('status', self.status, update_modified = update_modified)
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index e41ad428469..61700050614 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -86,6 +86,8 @@ class TestPurchaseInvoice(unittest.TestCase):
pe.submit()
pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name)
+ pi_doc.load_from_db()
+ self.assertTrue(pi_doc.status, "Paid")
self.assertRaises(frappe.LinkExistsError, pi_doc.cancel)
unlink_payment_on_cancel_of_invoice()
@@ -203,7 +205,9 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.insert()
pi.submit()
+ pi.load_from_db()
+ self.assertTrue(pi.status, "Unpaid")
self.check_gle_for_pi(pi.name)
def check_gle_for_pi(self, pi):
@@ -234,6 +238,9 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = frappe.copy_doc(test_records[0])
pi.insert()
+ pi.load_from_db()
+
+ self.assertTrue(pi.status, "Draft")
pi.naming_series = 'TEST-'
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
@@ -248,6 +255,8 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.get("taxes").pop(1)
pi.insert()
pi.submit()
+ pi.load_from_db()
+ self.assertTrue(pi.status, "Unpaid")
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
@@ -599,6 +608,11 @@ class TestPurchaseInvoice(unittest.TestCase):
# return entry
pi1 = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, rate=50, update_stock=1)
+ pi.load_from_db()
+ self.assertTrue(pi.status, "Debit Note Issued")
+ pi1.load_from_db()
+ self.assertTrue(pi1.status, "Return")
+
actual_qty_2 = get_qty_after_transaction()
self.assertEqual(actual_qty_1 - 2, actual_qty_2)
@@ -771,6 +785,8 @@ class TestPurchaseInvoice(unittest.TestCase):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount
pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
+ pi.load_from_db()
+ self.assertTrue(pi.status, "Return")
outstanding_amount = get_outstanding_amount(pi.doctype,
pi.name, "Creditors - _TC", pi.supplier, "Supplier")
diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py
index 16e918cc728..f371bd425c5 100755
--- a/erpnext/accounts/doctype/sales_invoice/pos.py
+++ b/erpnext/accounts/doctype/sales_invoice/pos.py
@@ -180,14 +180,16 @@ def get_items_list(pos_profile, company):
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
id.expense_account, id.selling_cost_center, id.default_warehouse,
- i.sales_uom, c.conversion_factor
+ i.sales_uom, c.conversion_factor, it.item_tax_template, it.valid_from
from
`tabItem` i
left join `tabItem Default` id on id.parent = i.name and id.company = %s
+ left join `tabItem Tax` it on it.parent = i.name
left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
where
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1
{cond}
+ group by i.item_code
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
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 d7e5114042d..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) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 52a0f4e081d..6e0a30d48e0 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",
@@ -396,7 +397,7 @@
{
"allow_on_submit": 1,
"fieldname": "po_no",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "Customer's Purchase Order",
"no_copy": 1,
"print_hide": 1
@@ -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-05-19 17:00:57.208696",
"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 5218b740edd..fa175fc816e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -785,7 +785,7 @@ class SalesInvoice(SellingController):
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
def make_tax_gl_entries(self, gl_entries):
@@ -802,7 +802,7 @@ class SalesInvoice(SellingController):
tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else
flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))),
"cost_center": tax.cost_center
- }, account_currency)
+ }, account_currency, item=tax)
)
def make_item_gl_entries(self, gl_entries):
@@ -822,7 +822,7 @@ class SalesInvoice(SellingController):
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
- gl_entries.append(self.get_gl_dict(gle))
+ gl_entries.append(self.get_gl_dict(gle, item=item))
asset.db_set("disposal_date", self.posting_date)
asset.set_status("Sold" if self.docstatus==1 else None)
@@ -860,7 +860,7 @@ class SalesInvoice(SellingController):
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- })
+ }, item=self)
)
gl_entries.append(
self.get_gl_dict({
@@ -869,7 +869,7 @@ class SalesInvoice(SellingController):
"against": self.customer,
"debit": self.loyalty_amount,
"remark": "Loyalty Points redeemed by the customer"
- })
+ }, item=self)
)
def make_pos_gl_entries(self, gl_entries):
@@ -890,7 +890,7 @@ class SalesInvoice(SellingController):
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
payment_mode_account_currency = get_account_currency(payment_mode.account)
@@ -903,7 +903,7 @@ class SalesInvoice(SellingController):
if payment_mode_account_currency==self.company_currency \
else payment_mode.amount,
"cost_center": self.cost_center
- }, payment_mode_account_currency)
+ }, payment_mode_account_currency, item=self)
)
def make_gle_for_change_amount(self, gl_entries):
@@ -921,7 +921,7 @@ class SalesInvoice(SellingController):
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
gl_entries.append(
@@ -930,7 +930,7 @@ class SalesInvoice(SellingController):
"against": self.customer,
"credit": self.base_change_amount,
"cost_center": self.cost_center
- })
+ }, item=self)
)
else:
frappe.throw(_("Select change amount account"), title="Mandatory Field")
@@ -954,7 +954,7 @@ class SalesInvoice(SellingController):
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center
- }, self.party_account_currency)
+ }, self.party_account_currency, item=self)
)
gl_entries.append(
self.get_gl_dict({
@@ -965,7 +965,7 @@ class SalesInvoice(SellingController):
self.precision("base_write_off_amount")) if write_off_account_currency==self.company_currency
else flt(self.write_off_amount, self.precision("write_off_amount"))),
"cost_center": self.cost_center or self.write_off_cost_center or default_cost_center
- }, write_off_account_currency)
+ }, write_off_account_currency, item=self)
)
def make_gle_for_rounding_adjustment(self, gl_entries):
@@ -982,8 +982,7 @@ class SalesInvoice(SellingController):
"credit": flt(self.base_rounding_adjustment,
self.precision("base_rounding_adjustment")),
"cost_center": self.cost_center or round_off_cost_center,
- }
- ))
+ }, item=self))
def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = []
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ced829ee6ee..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')
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/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js
index 24fcb41a5db..28c9149c561 100755
--- a/erpnext/accounts/page/pos/pos.js
+++ b/erpnext/accounts/page/pos/pos.js
@@ -1458,7 +1458,40 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
this.child.batch_no = this.item_batch_no[this.child.item_code];
this.child.serial_no = (this.item_serial_no[this.child.item_code]
? this.item_serial_no[this.child.item_code][0] : '');
- this.child.item_tax_rate = JSON.stringify(this.tax_data[this.child.item_code]);
+
+ const tax_template_is_valid = true;
+ if (this.items && this.items[0].valid_from) {
+ tax_template_is_valid = frappe.datetime.get_diff(frappe.datetime.now_date(),
+ this.items[0].valid_from) > 0;
+ }
+
+ this.child.item_tax_template = tax_template_is_valid ? this.items[0].item_tax_template : '';
+ this.child.item_tax_rate = JSON.stringify(this.tax_data[this.child.item_tax_template]);
+
+ if (this.child.item_tax_rate) {
+ this.add_taxes_from_item_tax_template(this.child.item_tax_rate);
+ }
+ },
+
+ add_taxes_from_item_tax_template: function(item_tax_map) {
+ let me = this;
+
+ if(item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
+ if(typeof (item_tax_map) == "string") {
+ item_tax_map = JSON.parse(item_tax_map);
+ }
+
+ $.each(item_tax_map, function(tax, rate) {
+ let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
+ if(!found) {
+ let child = frappe.model.add_child(me.frm.doc, "taxes");
+ child.charge_type = "On Net Total";
+ child.account_head = tax;
+ child.description = String(tax);
+ child.rate = rate;
+ }
+ });
+ }
},
update_paid_amount_status: function (update_paid_amount) {
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/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 39e218bfad2..49c1d0f2ccd 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -2,16 +2,19 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
+import datetime
+from six import iteritems
+
import frappe
from frappe import _
-from frappe.utils import flt
-from frappe.utils import formatdate
+from frappe.utils import flt, formatdate
+
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
-from six import iteritems
-from pprint import pprint
+
def execute(filters=None):
- if not filters: filters = {}
+ if not filters:
+ filters = {}
columns = get_columns(filters)
if filters.get("budget_against_filter"):
@@ -43,20 +46,25 @@ def execute(filters=None):
period_data[0] += last_total
- if(filters.get("show_cumulative")):
+ if filters.get("show_cumulative"):
last_total = period_data[0] - period_data[1]
period_data[2] = period_data[0] - period_data[1]
row += period_data
totals[2] = totals[0] - totals[1]
- if filters["period"] != "Yearly" :
+ if filters["period"] != "Yearly":
row += totals
data.append(row)
return columns, data
+
def get_columns(filters):
- columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"]
+ columns = [
+ _(filters.get("budget_against"))
+ + ":Link/%s:150" % (filters.get("budget_against")),
+ _("Account") + ":Link/Account:150"
+ ]
group_months = False if filters["period"] == "Monthly" else True
@@ -65,84 +73,181 @@ def get_columns(filters):
for year in fiscal_year:
for from_date, to_date in get_period_date_ranges(filters["period"], year[0]):
if filters["period"] == "Yearly":
- labels = [_("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), _("Variance ") + " " + str(year[0])]
+ labels = [
+ _("Budget") + " " + str(year[0]),
+ _("Actual ") + " " + str(year[0]),
+ _("Variance ") + " " + str(year[0])
+ ]
for label in labels:
- columns.append(label+":Float:150")
+ columns.append(label + ":Float:150")
else:
- for label in [_("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), _("Variance") + " (%s)" + " " + str(year[0])]:
+ for label in [
+ _("Budget") + " (%s)" + " " + str(year[0]),
+ _("Actual") + " (%s)" + " " + str(year[0]),
+ _("Variance") + " (%s)" + " " + str(year[0])
+ ]:
if group_months:
- label = label % (formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM"))
+ label = label % (
+ formatdate(from_date, format_string="MMM")
+ + "-"
+ + formatdate(to_date, format_string="MMM")
+ )
else:
label = label % formatdate(from_date, format_string="MMM")
- columns.append(label+":Float:150")
+ columns.append(label + ":Float:150")
- if filters["period"] != "Yearly" :
- return columns + [_("Total Budget") + ":Float:150", _("Total Actual") + ":Float:150",
- _("Total Variance") + ":Float:150"]
+ if filters["period"] != "Yearly":
+ return columns + [
+ _("Total Budget") + ":Float:150",
+ _("Total Actual") + ":Float:150",
+ _("Total Variance") + ":Float:150"
+ ]
else:
return columns
+
def get_cost_centers(filters):
- cond = "and 1=1"
+ order_by = ""
if filters.get("budget_against") == "Cost Center":
- cond = "order by lft"
+ order_by = "order by lft"
if filters.get("budget_against") in ["Cost Center", "Project"]:
- return frappe.db.sql_list("""select name from `tab{tab}` where company=%s
- {cond}""".format(tab=filters.get("budget_against"), cond=cond), filters.get("company"))
+ return frappe.db.sql_list(
+ """
+ select
+ name
+ from
+ `tab{tab}`
+ where
+ company = %s
+ {order_by}
+ """.format(tab=filters.get("budget_against"), order_by=order_by),
+ filters.get("company"))
else:
- return frappe.db.sql_list("""select name from `tab{tab}`""".format(tab=filters.get("budget_against"))) #nosec
+ return frappe.db.sql_list(
+ """
+ select
+ name
+ from
+ `tab{tab}`
+ """.format(tab=filters.get("budget_against"))) # nosec
-#Get dimension & target details
+
+# Get dimension & target details
def get_dimension_target_details(filters):
+ budget_against = frappe.scrub(filters.get("budget_against"))
cond = ""
if filters.get("budget_against_filter"):
- cond += " and b.{budget_against} in (%s)".format(budget_against = \
- frappe.scrub(filters.get('budget_against'))) % ', '.join(['%s']* len(filters.get('budget_against_filter')))
+ cond += """ and b.{budget_against} in (%s)""".format(
+ budget_against=budget_against) % ", ".join(["%s"] * len(filters.get("budget_against_filter")))
- return frappe.db.sql("""
- select b.{budget_against} as budget_against, b.monthly_distribution, ba.account, ba.budget_amount,b.fiscal_year
- from `tabBudget` b, `tabBudget Account` ba
- where b.name=ba.parent and b.docstatus = 1 and b.fiscal_year between %s and %s
- and b.budget_against = %s and b.company=%s {cond} order by b.fiscal_year
- """.format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond),
- tuple([filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company] + filters.get('budget_against_filter')),
- as_dict=True)
+ return frappe.db.sql(
+ """
+ select
+ b.{budget_against} as budget_against,
+ b.monthly_distribution,
+ ba.account,
+ ba.budget_amount,
+ b.fiscal_year
+ from
+ `tabBudget` b,
+ `tabBudget Account` ba
+ where
+ b.name = ba.parent
+ and b.docstatus = 1
+ and b.fiscal_year between %s and %s
+ and b.budget_against = %s
+ and b.company = %s
+ {cond}
+ order by
+ b.fiscal_year
+ """.format(
+ budget_against=budget_against,
+ cond=cond,
+ ),
+ tuple(
+ [
+ filters.from_fiscal_year,
+ filters.to_fiscal_year,
+ filters.budget_against,
+ filters.company,
+ ]
+ + filters.get("budget_against_filter")
+ ), as_dict=True)
-#Get target distribution details of accounts of cost center
+# Get target distribution details of accounts of cost center
def get_target_distribution_details(filters):
target_details = {}
- for d in frappe.db.sql("""select md.name, mdp.month, mdp.percentage_allocation
- from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
- where mdp.parent=md.name and md.fiscal_year between %s and %s order by md.fiscal_year""",(filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1):
- target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation))
+ for d in frappe.db.sql(
+ """
+ select
+ md.name,
+ mdp.month,
+ mdp.percentage_allocation
+ from
+ `tabMonthly Distribution Percentage` mdp,
+ `tabMonthly Distribution` md
+ where
+ mdp.parent = md.name
+ and md.fiscal_year between %s and %s
+ order by
+ md.fiscal_year
+ """,
+ (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1):
+ target_details.setdefault(d.name, {}).setdefault(
+ d.month, flt(d.percentage_allocation)
+ )
return target_details
-#Get actual details from gl entry
+# Get actual details from gl entry
def get_actual_details(name, filters):
- cond = "1=1"
- budget_against=filters.get("budget_against").replace(" ", "_").lower()
+ budget_against = frappe.scrub(filters.get("budget_against"))
+ cond = ""
if filters.get("budget_against") == "Cost Center":
cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"])
- cond = "lft>='{lft}' and rgt<='{rgt}'".format(lft = cc_lft, rgt=cc_rgt)
+ cond = """
+ and lft >= "{lft}"
+ and rgt <= "{rgt}"
+ """.format(lft=cc_lft, rgt=cc_rgt)
- ac_details = frappe.db.sql("""select gl.account, gl.debit, gl.credit,gl.fiscal_year,
- MONTHNAME(gl.posting_date) as month_name, b.{budget_against} as budget_against
- from `tabGL Entry` gl, `tabBudget Account` ba, `tabBudget` b
- where
- b.name = ba.parent
- and b.docstatus = 1
- and ba.account=gl.account
- and b.{budget_against} = gl.{budget_against}
- and gl.fiscal_year between %s and %s
- and b.{budget_against}=%s
- and exists(select name from `tab{tab}` where name=gl.{budget_against} and {cond}) group by gl.name order by gl.fiscal_year
- """.format(tab = filters.budget_against, budget_against = budget_against, cond = cond,from_year=filters.from_fiscal_year,to_year=filters.to_fiscal_year),
- (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1)
+ ac_details = frappe.db.sql(
+ """
+ select
+ gl.account,
+ gl.debit,
+ gl.credit,
+ gl.fiscal_year,
+ MONTHNAME(gl.posting_date) as month_name,
+ b.{budget_against} as budget_against
+ from
+ `tabGL Entry` gl,
+ `tabBudget Account` ba,
+ `tabBudget` b
+ where
+ b.name = ba.parent
+ and b.docstatus = 1
+ and ba.account=gl.account
+ and b.{budget_against} = gl.{budget_against}
+ and gl.fiscal_year between %s and %s
+ and b.{budget_against} = %s
+ and exists(
+ select
+ name
+ from
+ `tab{tab}`
+ where
+ name = gl.{budget_against}
+ {cond}
+ )
+ group by
+ gl.name
+ order by gl.fiscal_year
+ """.format(tab=filters.budget_against, budget_against=budget_against, cond=cond),
+ (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1)
cc_actual_details = {}
for d in ac_details:
@@ -151,7 +256,6 @@ def get_actual_details(name, filters):
return cc_actual_details
def get_dimension_account_month_map(filters):
- import datetime
dimension_target_details = get_dimension_target_details(filters)
tdd = get_target_distribution_details(filters)
@@ -161,28 +265,43 @@ def get_dimension_account_month_map(filters):
actual_details = get_actual_details(ccd.budget_against, filters)
for month_id in range(1, 13):
- month = datetime.date(2013, month_id, 1).strftime('%B')
- cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault(ccd.fiscal_year,{})\
- .setdefault(month, frappe._dict({
- "target": 0.0, "actual": 0.0
- }))
+ month = datetime.date(2013, month_id, 1).strftime("%B")
+ cam_map.setdefault(ccd.budget_against, {}).setdefault(
+ ccd.account, {}
+ ).setdefault(ccd.fiscal_year, {}).setdefault(
+ month, frappe._dict({"target": 0.0, "actual": 0.0})
+ )
tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month]
- month_percentage = tdd.get(ccd.monthly_distribution, {}).get(month, 0) \
- if ccd.monthly_distribution else 100.0/12
+ month_percentage = (
+ tdd.get(ccd.monthly_distribution, {}).get(month, 0)
+ if ccd.monthly_distribution
+ else 100.0 / 12
+ )
tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100
for ad in actual_details.get(ccd.account, []):
- if ad.month_name == month:
- tav_dict.actual += flt(ad.debit) - flt(ad.credit)
+ if ad.month_name == month and ad.fiscal_year == ccd.fiscal_year:
+ tav_dict.actual += flt(ad.debit) - flt(ad.credit)
return cam_map
+
def get_fiscal_years(filters):
- fiscal_year = frappe.db.sql("""select name from `tabFiscal Year` where
- name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
- {'from_fiscal_year': filters["from_fiscal_year"], 'to_fiscal_year': filters["to_fiscal_year"]})
+ fiscal_year = frappe.db.sql(
+ """
+ select
+ name
+ from
+ `tabFiscal Year`
+ where
+ name between %(from_fiscal_year)s and %(to_fiscal_year)s
+ """,
+ {
+ "from_fiscal_year": filters["from_fiscal_year"],
+ "to_fiscal_year": filters["to_fiscal_year"]
+ })
return fiscal_year
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 d3f3882299b..bb9cfcd886f 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 and gle.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
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/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js
index 622bab6946f..07752e1e626 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.js
+++ b/erpnext/accounts/report/trial_balance/trial_balance.js
@@ -46,7 +46,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"default": frappe.defaults.get_user_default("year_end_date"),
},
{
- "fieldname":"cost_center",
+ "fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@@ -61,7 +61,13 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
}
},
{
- "fieldname":"finance_book",
+ "fieldname": "project",
+ "label": __("Project"),
+ "fieldtype": "Link",
+ "options": "Project"
+ },
+ {
+ "fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book",
@@ -97,7 +103,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
}
erpnext.dimension_filters.forEach((dimension) => {
- frappe.query_reports["Trial Balance"].filters.splice(5, 0 ,{
+ frappe.query_reports["Trial Balance"].filters.splice(6, 0 ,{
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index d78324157a9..5a699b6580a 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -69,6 +69,11 @@ def get_data(filters):
gl_entries_by_account = {}
opening_balances = get_opening_balances(filters)
+
+ #add filter inside list so that the query in financial_statements.py doesn't break
+ if filters.project:
+ filters.project = [filters.project]
+
set_gl_entries_by_account(filters.company, filters.from_date,
filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry))
@@ -102,6 +107,9 @@ def get_rootwise_opening_balances(filters, report_type):
additional_conditions += """ and cost_center in (select name from `tabCost Center`
where lft >= %s and rgt <= %s)""" % (lft, rgt)
+ if filters.project:
+ additional_conditions += " and project = %(project)s"
+
if filters.finance_book:
fb_conditions = " AND finance_book = %(finance_book)s"
if filters.include_default_book_entries:
@@ -116,6 +124,7 @@ def get_rootwise_opening_balances(filters, report_type):
"from_date": filters.from_date,
"report_type": report_type,
"year_start_date": filters.year_start_date,
+ "project": filters.project,
"finance_book": filters.finance_book,
"company_fb": frappe.db.get_value("Company", filters.company, 'default_finance_book')
}
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..d56549f7d90 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,
@@ -468,7 +512,7 @@ class Asset(AccountsController):
"credit": self.purchase_receipt_amount,
"credit_in_account_currency": self.purchase_receipt_amount,
"cost_center": self.cost_center
- }))
+ }, item=self))
gl_entries.append(self.get_gl_dict({
"account": fixed_asset_account,
@@ -478,7 +522,7 @@ class Asset(AccountsController):
"debit": self.purchase_receipt_amount,
"debit_in_account_currency": self.purchase_receipt_amount,
"cost_center": self.cost_center
- }))
+ }, item=self))
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
@@ -611,7 +655,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
if asset:
account = get_asset_category_account(account_name, asset=asset,
asset_category = asset_category, company = company)
-
+
if not asset and not account:
account = get_asset_category_account(account_name, asset_category = asset_category, company = company)
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/change_log/v12/v12_9_0.md b/erpnext/change_log/v12/v12_9_0.md
new file mode 100644
index 00000000000..5d7e0587a34
--- /dev/null
+++ b/erpnext/change_log/v12/v12_9_0.md
@@ -0,0 +1,43 @@
+## ERPNext v12.9.0 Release Note
+
+### Enhancements
+- Pick List enhancements [#20962](https://github.com/frappe/erpnext/pull/20962)
+ - The purpose **Delivery Against Sales Order** has been changed to **Delivery** and patch for existing records.
+ - If the purpose is Delivery then allow rows without Sales Order against them to be mapped to the Delivery Note.
+ - **Update Current Stock** button to update locations and quantity after Submit.
+ - Validations if Item Locations table is empty (on creation of Delivery Note, Stock Entry).
+ - Company-wise fetching of locations and quantity.
+- Payment allocation on Payment Entry based on invoice payment terms. [#20945](https://github.com/frappe/erpnext/pull/20945)
+- Allow Tax Withholding Category selection at invoice level [#20870](https://github.com/frappe/erpnext/pull/20870)
+- Enhanced Employee Leave Balance report, added new fields New Allocation, Expired Leaves. [#21282](https://github.com/frappe/erpnext/pull/21282)
+- Provision to set Default Item Manufacturer. [#21197](https://github.com/frappe/erpnext/pull/21197)
+- Tax Amount in Credit Note print format should be shown in positive. [#21252](https://github.com/frappe/erpnext/pull/21252)
+- On creation of return from employee advance, sets default voucher type as Bank Entry and default debit account as Cash. [#21411](https://github.com/frappe/erpnext/pull/21411)
+- On saving a Contact linked with a Lead, update the contact info from Contact into the Lead. [#21469](https://github.com/frappe/erpnext/pull/21469)
+- Warning on making payment against paid invoices. [#21501](https://github.com/frappe/erpnext/pull/21501)
+- Enhanced Stock Balance report with color-coding. [#21516](https://github.com/frappe/erpnext/pull/21516)
+- Added total row in sales analytics report [#21519](https://github.com/frappe/erpnext/pull/21519)
+- Asset related accounts must be in company currency. [#21524](https://github.com/frappe/erpnext/pull/21524)
+- Accounting Dimensions in Period Closing Voucher. [#21564](https://github.com/frappe/erpnext/pull/21564)
+- Renamed LMS to Learning Management System. [#21645](https://github.com/frappe/erpnext/pull/21645)
+- Allow half-day attendance only via leave application. [#21719](https://github.com/frappe/erpnext/pull/21719)
+- In BOM, allowed Price List in other than company currency. [#21585](https://github.com/frappe/erpnext/pull/21585)
+
+### Fixes:
+- Account Type validation for accounts selected in Asset Category. [#21102](https://github.com/frappe/erpnext/pull/21102)
+- Warehouse unset if an item doesn't have a default warehouse. [#21285](https://github.com/frappe/erpnext/pull/21285)
+- Bin Requested Qty should be calculated for customer-provided items. [#21300](https://github.com/frappe/erpnext/pull/21300)
+- On change of item in sales and purchase transactions, UOM should be reset based on the item's default UOM. [#21254](https://github.com/frappe/erpnext/pull/21254)
+- Target warehouse in Delivery Note and Sales Invoice should not be set based on user permission. Patch to fix affected records. [#21359](https://github.com/frappe/erpnext/pull/21359)
+- Budget validation against an Accounting Dimension was missing. [#21268](https://github.com/frappe/erpnext/pull/21268)
+- On change of qty in Stock Entry, the fields in base currency were not set for non-serialized items. [#21389](https://github.com/frappe/erpnext/pull/21389)
+- Payment request not able to make against fees [#21486](https://github.com/frappe/erpnext/pull/21486)
+- Cost Center renaming is allowed only from Cost Center form [#21503](https://github.com/frappe/erpnext/pull/21503)
+- Accounts Payable report showing the advance amount of other company [#21548](https://github.com/frappe/erpnext/pull/21548)
+- Heatmap was not working for customer and supplier [#21578](https://github.com/frappe/erpnext/pull/21578)
+- Item tax template set in the Item master is not fetched into the sales invoice and taxes are not shown in the offline pos. [#21714](https://github.com/frappe/erpnext/pull/21714)
+- Validate duplicate creation of expiry ledger entry for carry forward allocation and patch to remove duplicate ledgers. [#21505](https://github.com/frappe/erpnext/pull/21505)
+- Dimensions were not getting added for some GL Entries and were missing in some recently added transactions. [#21689](https://github.com/frappe/erpnext/pull/21689)
+- In Repack entry, set rate for finished goods based on the cost of raw materials. [#21736](https://github.com/frappe/erpnext/pull/21736)
+- Removed Guest access from Lead. [#21692](https://github.com/frappe/erpnext/pull/21692)
+- Standard and Custom queries did not show search fields for Link fields. [#21685](https://github.com/frappe/erpnext/pull/21685)
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 95fe6a2f3fa..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)
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..73ed4b01686 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -8,11 +8,14 @@ from frappe.desk.reportview import get_match_cond, get_filters_cond
from frappe.utils import nowdate, getdate
from collections import defaultdict
from erpnext.stock.get_item_details import _get_item_tax_template
+from frappe.utils import unique
# searches for active employees
def employee_query(doctype, txt, searchfield, start, page_len, filters):
conditions = []
- return frappe.db.sql("""select name, employee_name from `tabEmployee`
+ fields = get_fields("Employee", ["name", "employee_name"])
+
+ return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active'
and docstatus < 2
and ({key} like %(txt)s
@@ -24,6 +27,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
idx desc,
name, employee_name
limit %(start)s, %(page_len)s""".format(**{
+ 'fields': ", ".join(fields),
'key': searchfield,
'fcond': get_filters_cond(doctype, filters, conditions),
'mcond': get_match_cond(doctype)
@@ -34,9 +38,12 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
'page_len': page_len
})
- # searches for leads which are not converted
+
+# searches for leads which are not converted
def lead_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql("""select name, lead_name, company_name from `tabLead`
+ fields = get_fields("Lead", ["name", "lead_name", "company_name"])
+
+ return frappe.db.sql("""select {fields} from `tabLead`
where docstatus < 2
and ifnull(status, '') != 'Converted'
and ({key} like %(txt)s
@@ -50,6 +57,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
idx desc,
name, lead_name
limit %(start)s, %(page_len)s""".format(**{
+ 'fields': ", ".join(fields),
'key': searchfield,
'mcond':get_match_cond(doctype)
}), {
@@ -59,6 +67,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
'page_len': page_len
})
+
# searches for customer
def customer_query(doctype, txt, searchfield, start, page_len, filters):
conditions = []
@@ -69,13 +78,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
else:
fields = ["name", "customer_name", "customer_group", "territory"]
- meta = frappe.get_meta("Customer")
- searchfields = meta.get_search_fields()
- searchfields = searchfields + [f for f in [searchfield or "name", "customer_name"] \
- if not f in searchfields]
- fields = fields + [f for f in searchfields if not f in fields]
+ fields = get_fields("Customer", fields)
- fields = ", ".join(fields)
+ searchfields = frappe.get_meta("Customer").get_search_fields()
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
return frappe.db.sql("""select {fields} from `tabCustomer`
@@ -88,7 +93,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
idx desc,
name, customer_name
limit %(start)s, %(page_len)s""".format(**{
- "fields": fields,
+ "fields": ", ".join(fields),
"scond": searchfields,
"mcond": get_match_cond(doctype),
"fcond": get_filters_cond(doctype, filters, conditions).replace('%', '%%'),
@@ -99,6 +104,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
'page_len': page_len
})
+
# searches for supplier
def supplier_query(doctype, txt, searchfield, start, page_len, filters):
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
@@ -106,7 +112,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
fields = ["name", "supplier_group"]
else:
fields = ["name", "supplier_name", "supplier_group"]
- fields = ", ".join(fields)
+
+ fields = get_fields("Supplier", fields)
return frappe.db.sql("""select {field} from `tabSupplier`
where docstatus < 2
@@ -119,7 +126,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
idx desc,
name, supplier_name
limit %(start)s, %(page_len)s """.format(**{
- 'field': fields,
+ 'field': ', '.join(fields),
'key': searchfield,
'mcond':get_match_cond(doctype)
}), {
@@ -129,6 +136,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
'page_len': page_len
})
+
def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
company_currency = erpnext.get_company_currency(filters.get('company'))
@@ -153,6 +161,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
return tax_accounts
+
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
conditions = []
@@ -214,10 +223,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
"page_len": page_len
}, as_dict=as_dict)
+
def bom(doctype, txt, searchfield, start, page_len, filters):
conditions = []
+ fields = get_fields("BOM", ["name", "item"])
- return frappe.db.sql("""select tabBOM.name, tabBOM.item
+ return frappe.db.sql("""select {fields}
from tabBOM
where tabBOM.docstatus=1
and tabBOM.is_active=1
@@ -227,6 +238,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc, name
limit %(start)s, %(page_len)s """.format(
+ fields=", ".join(fields),
fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'),
mcond=get_match_cond(doctype).replace('%', '%%'),
key=searchfield),
@@ -237,13 +249,16 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
'page_len': page_len or 20
})
+
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
cond = ''
if filters.get('customer'):
cond = """(`tabProject`.customer = %s or
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))
- return frappe.db.sql("""select `tabProject`.name from `tabProject`
+ fields = get_fields("Project", ["name"])
+
+ return frappe.db.sql("""select {fields} from `tabProject`
where `tabProject`.status not in ("Completed", "Cancelled")
and {cond} `tabProject`.name like %(txt)s {match_cond}
order by
@@ -251,6 +266,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
idx desc,
`tabProject`.name asc
limit {start}, {page_len}""".format(
+ fields=", ".join(['`tabProject`.{0}'.format(f) for f in fields]),
cond=cond,
match_cond=get_match_cond(doctype),
start=start,
@@ -261,8 +277,10 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
+ fields = get_fields("Delivery Note", ["name", "customer", "posting_date"])
+
return frappe.db.sql("""
- select `tabDelivery Note`.name, `tabDelivery Note`.customer, `tabDelivery Note`.posting_date
+ select %(fields)s
from `tabDelivery Note`
where `tabDelivery Note`.`%(key)s` like %(txt)s and
`tabDelivery Note`.docstatus = 1
@@ -277,6 +295,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
)
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s
""" % {
+ "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]),
"key": searchfield,
"fcond": get_filters_cond(doctype, filters, []),
"mcond": get_match_cond(doctype),
@@ -342,6 +361,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
order by expiry_date, name desc
limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args)
+
def get_account_list(doctype, txt, searchfield, start, page_len, filters):
filter_list = []
@@ -365,6 +385,21 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
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):
from erpnext.controllers.queries import get_match_cond
@@ -470,6 +505,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters)
+
@frappe.whitelist()
def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters):
item_filters = [
@@ -487,6 +523,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters)
)
return item_manufacturers
+
@frappe.whitelist()
def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
query = """
@@ -500,6 +537,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters)
+
@frappe.whitelist()
def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
query = """
@@ -513,6 +551,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters)
+
@frappe.whitelist()
def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
@@ -536,3 +575,13 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
taxes = _get_item_tax_template(args, taxes, for_validate=True)
return [(d,) for d in set(taxes)]
+
+
+def get_fields(doctype, fields=[]):
+ meta = frappe.get_meta(doctype)
+ fields.extend(meta.get_search_fields())
+
+ if meta.title_field and not meta.title_field.strip() in fields:
+ fields.insert(1, meta.title_field.strip())
+
+ return unique(fields)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index de771a55f2c..c25ad060674 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -165,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
@@ -182,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
@@ -192,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):
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index de76e45cd11..b465a106f0e 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -69,17 +69,6 @@ status_map = {
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
],
- "Purchase Invoice": [
- ["Draft", None],
- ["Submitted", "eval:self.docstatus==1"],
- ["Paid", "eval:self.outstanding_amount==0 and self.docstatus==1"],
- ["Return", "eval:self.is_return==1 and self.docstatus==1"],
- ["Debit Note Issued",
- "eval:self.outstanding_amount <= 0 and self.docstatus==1 and self.is_return==0 and get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1})"],
- ["Unpaid", "eval:self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.docstatus==1"],
- ["Overdue", "eval:self.outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.docstatus==1"],
- ["Cancelled", "eval:self.docstatus==2"],
- ],
"Material Request": [
["Draft", None],
["Stopped", "eval:self.status == 'Stopped'"],
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/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index eb68c679ba5..d0565d19b8d 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -366,7 +366,7 @@
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
- "modified": "2019-09-19 12:49:02.536647",
+ "modified": "2020-05-11 20:30:02.536647",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -423,15 +423,6 @@
"read": 1,
"report": 1,
"role": "Sales User"
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Guest",
- "share": 1
}
],
"search_fields": "lead_name,lead_owner,status",
@@ -439,4 +430,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "lead_name"
-}
\ No newline at end of file
+}
diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json
index 4374bb58314..fe5fce32f86 100644
--- a/erpnext/crm/doctype/sales_stage/sales_stage.json
+++ b/erpnext/crm/doctype/sales_stage/sales_stage.json
@@ -1,96 +1,44 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:stage_name",
- "beta": 0,
- "creation": "2018-10-01 09:28:16.399518",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:stage_name",
+ "creation": "2018-10-01 09:28:16.399518",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "stage_name"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "stage_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": "Stage 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,
- "translatable": 0,
+ "fieldname": "stage_name",
+ "fieldtype": "Data",
+ "label": "Stage Name",
"unique": 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": 0,
- "max_attachments": 0,
- "modified": "2018-10-01 09:29:43.230378",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "Sales Stage",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2020-05-20 14:39:33.300588",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Sales Stage",
+ "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": "Sales Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
"write": 1
}
- ],
- "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
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py
new file mode 100644
index 00000000000..95b19ec21ec
--- /dev/null
+++ b/erpnext/crm/utils.py
@@ -0,0 +1,23 @@
+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.db_set("phone", phone)
+ lead.db_set("mobile_no", mobile_no)
diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.json b/erpnext/education/doctype/assessment_plan/assessment_plan.json
index bc3946440c3..c7df672e71b 100644
--- a/erpnext/education/doctype/assessment_plan/assessment_plan.json
+++ b/erpnext/education/doctype/assessment_plan/assessment_plan.json
@@ -1,790 +1,207 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
- "allow_rename": 0,
"autoname": "EDU-ASP-.YYYY.-.#####",
- "beta": 0,
"creation": "2015-11-12 16:34:34.658092",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "student_group",
+ "assessment_name",
+ "assessment_group",
+ "grading_scale",
+ "column_break_2",
+ "course",
+ "program",
+ "academic_year",
+ "academic_term",
+ "section_break_5",
+ "schedule_date",
+ "room",
+ "examiner",
+ "examiner_name",
+ "column_break_4",
+ "from_time",
+ "to_time",
+ "supervisor",
+ "supervisor_name",
+ "section_break_20",
+ "maximum_assessment_score",
+ "assessment_criteria",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "student_group",
"fieldtype": "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": 1,
"label": "Student Group",
- "length": 0,
- "no_copy": 0,
"options": "Student Group",
- "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,
"fieldname": "assessment_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assessment 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,
- "translatable": 0,
- "unique": 0
+ "label": "Assessment Name"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "assessment_group",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Assessment Group",
- "length": 0,
- "no_copy": 0,
"options": "Assessment Group",
- "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_from": "course.default_grading_scale",
+ "fetch_if_empty": 1,
"fieldname": "grading_scale",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Grading Scale",
- "length": 0,
- "no_copy": 0,
"options": "Grading Scale",
- "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,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "student_group.course",
+ "fetch_if_empty": 1,
"fieldname": "course",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Course",
- "length": 0,
- "no_copy": 0,
"options": "Course",
- "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_from": "student_group.program",
"fieldname": "program",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Program",
- "length": 0,
- "no_copy": 0,
- "options": "Program",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Program"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "student_group.academic_year",
"fieldname": "academic_year",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Academic Year",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Year",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Academic Year"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "student_group.academic_term",
"fieldname": "academic_term",
"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": "Academic Term",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Academic Term"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "collapsible_depends_on": "",
- "columns": 0,
- "depends_on": "",
"fieldname": "section_break_5",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Schedule",
- "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": "Schedule"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Today",
"fieldname": "schedule_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": "Schedule Date",
- "length": 0,
"no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 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,
"fieldname": "room",
"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": "Room",
- "length": 0,
- "no_copy": 0,
- "options": "Room",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Room"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "examiner",
"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": "Examiner",
- "length": 0,
- "no_copy": 0,
- "options": "Instructor",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Instructor"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "examiner.instructor_name",
"fieldname": "examiner_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": "Examiner 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
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_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": 0,
"fieldname": "from_time",
"fieldtype": "Time",
- "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": "From Time",
- "length": 0,
"no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 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,
"fieldname": "to_time",
"fieldtype": "Time",
- "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": "To Time",
- "length": 0,
"no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 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,
"fieldname": "supervisor",
"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": "Supervisor",
- "length": 0,
- "no_copy": 0,
- "options": "Instructor",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Instructor"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "supervisor.instructor_name",
"fieldname": "supervisor_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Supervisor 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
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_20",
"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": "Evaluate",
- "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": "Evaluate"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "maximum_assessment_score",
"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": "Maximum Assessment Score",
- "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
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "assessment_criteria",
"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": "Assessment Criteria",
- "length": 0,
- "no_copy": 0,
"options": "Assessment Plan Criteria",
- "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,
"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": "Assessment Plan",
- "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
+ "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,
- "menu_index": 0,
- "modified": "2018-08-30 00:48:03.475522",
+ "links": [],
+ "modified": "2020-05-09 16:44:04.313175",
"modified_by": "Administrator",
"module": "Education",
"name": "Assessment Plan",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
@@ -794,28 +211,17 @@
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Academics User",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
"restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "assessment_name",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "title_field": "assessment_name"
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/education_settings/education_settings.json b/erpnext/education/doctype/education_settings/education_settings.json
index 967a030fd2a..8ba189e93a1 100644
--- a/erpnext/education/doctype/education_settings/education_settings.json
+++ b/erpnext/education/doctype/education_settings/education_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2017-04-05 13:33:04.519313",
"doctype": "DocType",
"editable_grid": 1,
@@ -42,12 +43,14 @@
"fieldtype": "Column Break"
},
{
+ "default": "0",
"description": "For Batch based Student Group, the Student Batch will be validated for every Student from the Program Enrollment.",
"fieldname": "validate_batch",
"fieldtype": "Check",
"label": "Validate Batch for Students in Student Group"
},
{
+ "default": "0",
"description": "For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment.",
"fieldname": "validate_course",
"fieldtype": "Check",
@@ -74,13 +77,13 @@
{
"fieldname": "web_academy_settings_section",
"fieldtype": "Section Break",
- "label": "LMS Settings"
+ "label": "Learning Management System Settings"
},
{
"depends_on": "eval: doc.enable_lms",
"fieldname": "portal_title",
"fieldtype": "Data",
- "label": "LMS Title"
+ "label": "Learning Management System Title"
},
{
"depends_on": "eval: doc.enable_lms",
@@ -89,9 +92,10 @@
"label": "Description"
},
{
+ "default": "0",
"fieldname": "enable_lms",
"fieldtype": "Check",
- "label": "Enable LMS"
+ "label": "Enable Learning Management System"
},
{
"default": "0",
@@ -102,7 +106,8 @@
}
],
"issingle": 1,
- "modified": "2019-05-13 18:36:13.127563",
+ "links": [],
+ "modified": "2020-05-07 19:50:22.430576",
"modified_by": "Administrator",
"module": "Education",
"name": "Education Settings",
@@ -141,4 +146,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
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..deb3968ecc7 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)))
@@ -96,14 +97,16 @@ class Fees(AccountsController):
"debit_in_account_currency": self.grand_total,
"against_voucher": self.name,
"against_voucher_type": self.doctype
- })
+ }, item=self)
+
fee_gl_entry = self.get_gl_dict({
"account": self.income_account,
"against": self.student,
"credit": self.grand_total,
"credit_in_account_currency": self.grand_total,
"cost_center": self.cost_center
- })
+ }, item=self)
+
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries([student_gl_entries, fee_gl_entry], cancel=(self.docstatus == 2),
update_outstanding="Yes", merge_entries=False)
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/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 970793f7468..c175eaaa251 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -209,7 +209,7 @@ def new_bank_transaction(transaction):
result.append(new_transaction.name)
except Exception:
- frappe.throw(frappe.get_traceback())
+ frappe.throw(title=_('Bank transaction creation error'))
return result
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 316c3392851..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"
@@ -268,7 +269,8 @@ doc_events = {
scheduler_events = {
"all": [
- "erpnext.projects.doctype.project.project.project_status_update_reminder"
+ "erpnext.projects.doctype.project.project.project_status_update_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',
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index e612c3d7572..f23b4220849 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -38,7 +38,7 @@ class Attendance(Document):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
# leaves can be marked for future dates
- if self.status not in ('On Leave', 'Half Day') and getdate(self.attendance_date) > getdate(nowdate()):
+ if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()):
frappe.throw(_("Attendance can not be marked for future dates"))
elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining):
frappe.throw(_("Attendance date can not be less than employee's joining date"))
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/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json
index 2dd6c10988d..26df6185444 100644
--- a/erpnext/hr/doctype/employee_other_income/employee_other_income.json
+++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json
@@ -76,25 +76,15 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-03-19 18:06:45.361830",
+ "modified": "2020-05-18 17:17:38.883126",
"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
- },
- {
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -104,9 +94,12 @@
"report": 1,
"role": "HR Manager",
"share": 1,
+ "submit": 1,
"write": 1
},
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -116,9 +109,12 @@
"report": 1,
"role": "HR User",
"share": 1,
+ "submit": 1,
"write": 1
},
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -128,6 +124,7 @@
"report": 1,
"role": "Employee",
"share": 1,
+ "submit": 1,
"write": 1
}
],
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index fefec01130a..4d97b0d0c72 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -115,8 +115,9 @@ class ExpenseClaim(AccountsController):
"party_type": "Employee",
"party": self.employee,
"against_voucher_type": self.doctype,
- "against_voucher": self.name
- })
+ "against_voucher": self.name,
+ "cost_center": self.cost_center
+ }, item=self)
)
# expense entries
@@ -127,8 +128,8 @@ class ExpenseClaim(AccountsController):
"debit": data.sanctioned_amount,
"debit_in_account_currency": data.sanctioned_amount,
"against": self.employee,
- "cost_center": self.cost_center
- })
+ "cost_center": data.cost_center
+ }, item=data)
)
for data in self.advances:
@@ -156,7 +157,7 @@ class ExpenseClaim(AccountsController):
"credit": self.grand_total,
"credit_in_account_currency": self.grand_total,
"against": self.employee
- })
+ }, item=self)
)
gl_entry.append(
@@ -169,7 +170,7 @@ class ExpenseClaim(AccountsController):
"debit_in_account_currency": self.grand_total,
"against_voucher": self.name,
"against_voucher_type": self.doctype,
- })
+ }, item=self)
)
return gl_entry
@@ -186,7 +187,7 @@ class ExpenseClaim(AccountsController):
"cost_center": self.cost_center,
"against_voucher_type": self.doctype,
"against_voucher": self.name
- })
+ }, item=tax)
)
def validate_account_details(self):
diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
index b23fb6af0fe..518e0aeefac 100644
--- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
+++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
@@ -10,6 +10,22 @@
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "expense_date",
+ "column_break_2",
+ "expense_type",
+ "default_account",
+ "section_break_4",
+ "description",
+ "section_break_6",
+ "amount",
+ "column_break_8",
+ "sanctioned_amount",
+ "accounting_dimensions_section",
+ "cost_center",
+ "dimension_col_break"
+ ],
"fields": [
{
"allow_bulk_edit": 0,
@@ -348,6 +364,21 @@
"translatable": 0,
"unique": 0,
"width": "150px"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
}
],
"has_web_view": 0,
@@ -359,12 +390,12 @@
"is_submittable": 0,
"issingle": 0,
"istable": 1,
- "max_attachments": 0,
- "modified": "2019-06-10 08:41:36.122565",
+ "links": [],
+ "modified": "2020-05-11 18:54:35.601592",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim Detail",
- "owner": "harshada@webnotestech.com",
+ "owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
index 1b1889b205f..f6a1e1bfe25 100644
--- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
+++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
@@ -7,14 +7,16 @@
"engine": "InnoDB",
"field_order": [
"account_head",
- "cost_center",
"col_break1",
"rate",
"description",
"section_break_6",
"tax_amount",
"column_break_8",
- "total"
+ "total",
+ "accounting_dimensions_section",
+ "cost_center",
+ "dimension_col_break"
],
"fields": [
{
@@ -90,10 +92,20 @@
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
- "modified": "2019-06-20 12:01:33.919555",
+ "links": [],
+ "modified": "2020-05-11 19:01:26.611758",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Taxes and Charges",
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/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index c870cc5d5d0..6ef4a303d87 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -541,7 +541,7 @@ def get_remaining_leaves(allocation, leaves_taken, date, expiry):
return _get_remaining_leaves(total_leaves, allocation.to_date)
-def get_leaves_for_period(employee, leave_type, from_date, to_date):
+def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0
@@ -551,8 +551,8 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date):
if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
leave_days += leave_entry.leaves
- elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \
- and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date):
+ elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \
+ and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)):
leave_days += leave_entry.leaves
elif leave_entry.transaction_type == 'Leave Application':
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
index 9ed58c9e59b..63559c4f5ae 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -88,32 +88,40 @@ def get_previous_expiry_ledger_entry(ledger):
}, fieldname=['name'])
def process_expired_allocation():
- ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry '''
+ ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry
+ Case 1: carry forwarded expiry period is set for the leave type,
+ create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves
+ Case 2: leave type has no specific expiry period for carry forwarded leaves
+ and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves.
+ '''
# fetch leave type records that has carry forwarded leaves expiry
leave_type_records = frappe.db.get_values("Leave Type", filters={
'expire_carry_forwarded_leaves_after_days': (">", 0)
}, fieldname=['name'])
- leave_type = [record[0] for record in leave_type_records]
+ leave_type = [record[0] for record in leave_type_records] or ['']
- expired_allocation = frappe.db.sql_list("""SELECT name
- FROM `tabLeave Ledger Entry`
- WHERE
- `transaction_type`='Leave Allocation'
- AND `is_expired`=1""")
-
- expire_allocation = frappe.get_all("Leave Ledger Entry",
- fields=['leaves', 'to_date', 'employee', 'leave_type', 'is_carry_forward', 'transaction_name as name', 'transaction_type'],
- filters={
- 'to_date': ("<", today()),
- 'transaction_type': 'Leave Allocation',
- 'transaction_name': ('not in', expired_allocation)
- },
- or_filters={
- 'is_carry_forward': 0,
- 'leave_type': ('in', leave_type)
- })
+ # fetch non expired leave ledger entry of transaction_type allocation
+ expire_allocation = frappe.db.sql("""
+ SELECT
+ leaves, to_date, employee, leave_type,
+ is_carry_forward, transaction_name as name, transaction_type
+ FROM `tabLeave Ledger Entry` l
+ WHERE (NOT EXISTS
+ (SELECT name
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ transaction_name = l.transaction_name
+ AND transaction_type = 'Leave Allocation'
+ AND name<>l.name
+ AND docstatus = 1
+ AND (
+ is_carry_forward=l.is_carry_forward
+ OR (is_carry_forward = 0 AND leave_type not in %s)
+ )))
+ AND transaction_type = 'Leave Allocation'
+ AND to_date < %s""", (leave_type, today()), as_dict=1)
if expire_allocation:
create_expiry_ledger_entry(expire_allocation)
@@ -133,6 +141,7 @@ def get_remaining_leaves(allocation):
'employee': allocation.employee,
'leave_type': allocation.leave_type,
'to_date': ('<=', allocation.to_date),
+ 'docstatus': 1
}, fieldname=['SUM(leaves)'])
@frappe.whitelist()
@@ -159,7 +168,8 @@ def expire_allocation(allocation, expiry_date=None):
def expire_carried_forward_allocation(allocation):
''' Expires remaining leaves in the on carried forward allocation '''
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
- leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date)
+ leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type,
+ allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True)
leaves = flt(allocation.leaves) + flt(leaves_taken)
# allow expired leaves entry to be created
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/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py
index df76458fe02..5ba7f1c4327 100644
--- a/erpnext/hr/doctype/salary_structure/salary_structure.py
+++ b/erpnext/hr/doctype/salary_structure/salary_structure.py
@@ -149,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,
@@ -169,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/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/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 4f08bbc3fc7..74169c80d35 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -175,6 +175,12 @@ frappe.ui.form.on("BOM", {
});
},
+ rm_cost_as_per: function(frm) {
+ if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) {
+ frm.set_value("plc_conversion_rate", 1.0);
+ }
+ },
+
routing: function(frm) {
if (frm.doc.routing) {
frappe.call({
@@ -205,7 +211,7 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({
item_code: function(doc, cdt, cdn){
var scrap_items = false;
var child = locals[cdt][cdn];
- if(child.doctype == 'BOM Scrap Item') {
+ if (child.doctype == 'BOM Scrap Item') {
scrap_items = true;
}
@@ -215,8 +221,19 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({
get_bom_material_detail(doc, cdt, cdn, scrap_items);
},
+
+ buying_price_list: function(doc) {
+ this.apply_price_list();
+ },
+
+ plc_conversion_rate: function(doc) {
+ if (!this.in_apply_price_list) {
+ this.apply_price_list(null, true);
+ }
+ },
+
conversion_factor: function(doc, cdt, cdn) {
- if(frappe.meta.get_docfield(cdt, "stock_qty", cdn)) {
+ if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) {
var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["qty", "conversion_factor"]);
item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 63f4f977c59..4ce0ecf3f27 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"creation": "2013-01-22 15:11:38",
"doctype": "DocType",
@@ -6,23 +7,25 @@
"engine": "InnoDB",
"field_order": [
"item",
- "quantity",
- "set_rate_of_sub_assembly_item_based_on_bom",
+ "company",
+ "item_name",
+ "uom",
"cb0",
"is_active",
"is_default",
"allow_alternative_item",
- "image",
- "item_name",
- "uom",
- "currency_detail",
- "company",
+ "set_rate_of_sub_assembly_item_based_on_bom",
"project",
+ "quantity",
+ "image",
+ "currency_detail",
+ "currency",
"conversion_rate",
"column_break_12",
- "currency",
"rm_cost_as_per",
"buying_price_list",
+ "price_list_currency",
+ "plc_conversion_rate",
"section_break_21",
"with_operations",
"column_break_23",
@@ -176,7 +179,8 @@
},
{
"fieldname": "currency_detail",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Currency and Price List"
},
{
"fieldname": "company",
@@ -324,7 +328,7 @@
},
{
"fieldname": "base_scrap_material_cost",
- "fieldtype": "Data",
+ "fieldtype": "Currency",
"label": "Scrap Material Cost(Company Currency)",
"no_copy": 1,
"options": "Company:company:default_currency",
@@ -477,13 +481,31 @@
{
"fieldname": "column_break_52",
"fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "plc_conversion_rate",
+ "fieldtype": "Float",
+ "label": "Price List Exchange Rate"
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "price_list_currency",
+ "fieldtype": "Link",
+ "label": "Price List Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-sitemap",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
- "modified": "2019-11-22 14:35:12.142150",
+ "links": [],
+ "modified": "2020-05-05 14:29:32.634952",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 19d36f6e8c4..d11077270aa 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -55,11 +55,13 @@ class BOM(WebsiteGenerator):
self.validate_main_item()
self.validate_currency()
self.set_conversion_rate()
+ self.set_plc_conversion_rate()
self.validate_uom_is_interger()
self.set_bom_material_details()
self.validate_materials()
self.validate_operations()
self.calculate_cost()
+ self.update_cost(update_parent=False, from_child_bom=True, save=False)
def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@@ -155,7 +157,7 @@ class BOM(WebsiteGenerator):
'rate' : rate,
'qty' : args.get("qty") or args.get("stock_qty") or 1,
'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
- 'base_rate' : rate,
+ 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1),
'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0
}
@@ -183,7 +185,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":
@@ -216,7 +218,7 @@ class BOM(WebsiteGenerator):
frappe.msgprint(_("{0} not found for item {1}")
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
- return flt(rate) / (self.conversion_rate or 1)
+ return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
if self.docstatus == 2:
@@ -233,15 +235,21 @@ class BOM(WebsiteGenerator):
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor
})
+
if rate:
d.rate = rate
d.amount = flt(d.rate) * flt(d.qty)
+ d.base_rate = flt(d.rate) * flt(self.conversion_rate)
+ d.base_amount = flt(d.amount) * flt(self.conversion_rate)
+
+ if save:
+ 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
@@ -361,6 +369,13 @@ class BOM(WebsiteGenerator):
elif self.conversion_rate == 1 or flt(self.conversion_rate) <= 0:
self.conversion_rate = get_exchange_rate(self.currency, self.company_currency(), args="for_buying")
+ def set_plc_conversion_rate(self):
+ if self.rm_cost_as_per in ["Valuation Rate", "Last Purchase Rate"]:
+ self.plc_conversion_rate = 1
+ elif not self.plc_conversion_rate and self.price_list_currency:
+ self.plc_conversion_rate = get_exchange_rate(self.price_list_currency,
+ self.company_currency(), args="for_buying")
+
def validate_materials(self):
""" Validate raw material entries """
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 45a7b935d38..3dfd03b1395 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -81,13 +81,13 @@ class TestBOM(unittest.TestCase):
# test amounts in selected currency
self.assertEqual(bom.operating_cost, 100)
- self.assertEqual(bom.raw_material_cost, 8000)
- self.assertEqual(bom.total_cost, 8100)
+ self.assertEqual(bom.raw_material_cost, 351.68)
+ self.assertEqual(bom.total_cost, 451.68)
# test amounts in selected currency
self.assertEqual(bom.base_operating_cost, 6000)
- self.assertEqual(bom.base_raw_material_cost, 480000)
- self.assertEqual(bom.base_total_cost, 486000)
+ self.assertEqual(bom.base_raw_material_cost, 21100.80)
+ self.assertEqual(bom.base_total_cost, 27100.80)
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
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.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 4c1ab7479fb..8fce3161375 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -107,30 +107,31 @@ class JobCard(Document):
for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], []
-
+ field = "operation_id" if self.operation_id else "operation"
data = frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order,
- "workstation": self.workstation, "operation": self.operation})
+ "workstation": self.workstation, field: self.get(field)})
if data and len(data) > 0:
for_quantity = data[0].completed_qty
time_in_mins = data[0].time_in_mins
- if for_quantity:
+ if self.get(field):
time_data = frappe.db.sql("""
SELECT
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s
- and jc.workstation = %s and jc.operation = %s and jc.docstatus = 1
- """, (self.work_order, self.workstation, self.operation), as_dict=1)
+ and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1
+ """.format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1)
wo = frappe.get_doc('Work Order', self.work_order)
+ work_order_field = "name" if field == "operation_id" else field
for data in wo.operations:
- if data.workstation == self.workstation and data.operation == self.operation:
+ if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation:
data.completed_qty = for_quantity
data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None
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 2d8d75d1f94..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);
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 8a39f7420e9..a7e1acc524c 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -382,6 +382,9 @@ class WorkOrder(Document):
return holidays[holiday_list]
def update_operation_status(self):
+ allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order"))
+ max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage/100 * flt(self.qty))
+
for d in self.get("operations"):
if not d.completed_qty:
d.status = "Pending"
@@ -389,6 +392,8 @@ class WorkOrder(Document):
d.status = "Work in Progress"
elif flt(d.completed_qty) == flt(self.qty):
d.status = "Completed"
+ elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
+ d.status = "Completed"
else:
frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'"))
@@ -809,4 +814,4 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
doc.set_item_locations()
- return doc
\ No newline at end of file
+ return doc
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 89b5d54c199..ea12e2f2210 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -625,7 +625,7 @@ erpnext.patches.v11_1.update_default_supplier_in_item_defaults
erpnext.patches.v12_0.update_due_date_in_gle
erpnext.patches.v12_0.add_default_buying_selling_terms_in_company
erpnext.patches.v12_0.update_ewaybill_field_position
-erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes
+erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes #2020-05-11
erpnext.patches.v11_1.set_status_for_material_request_type_manufacture
erpnext.patches.v12_0.move_plaid_settings_to_doctype
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_link')
@@ -652,10 +652,18 @@ 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
+erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries
+execute:frappe.delete_doc_if_exists("Page", "appointment-analytic")
+erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price
+erpnext.patches.v12_0.set_serial_no_status
+erpnext.patches.v12_0.update_price_list_currency_in_bom
diff --git a/erpnext/patches/v11_0/set_default_email_template_in_hr.py b/erpnext/patches/v11_0/set_default_email_template_in_hr.py
index 14954fbeb31..46223761095 100644
--- a/erpnext/patches/v11_0/set_default_email_template_in_hr.py
+++ b/erpnext/patches/v11_0/set_default_email_template_in_hr.py
@@ -1,8 +1,9 @@
from __future__ import unicode_literals
+from frappe import _
import frappe
def execute():
hr_settings = frappe.get_single("HR Settings")
- hr_settings.leave_approval_notification_template = "Leave Approval Notification"
- hr_settings.leave_status_notification_template = "Leave Status Notification"
- hr_settings.save()
\ No newline at end of file
+ hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
+ hr_settings.leave_status_notification_template = _("Leave Status Notification")
+ hr_settings.save()
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/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py
index b71ea665943..657decfed23 100644
--- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py
+++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py
@@ -20,7 +20,8 @@ def execute():
else:
insert_after_field = 'accounting_dimensions_section'
- for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item"]:
+ for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item",
+ "Expense Claim Detail", "Expense Taxes and Charges"]:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
diff --git a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py
new file mode 100644
index 00000000000..98a2fcf27ea
--- /dev/null
+++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2018, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ """Delete duplicate leave ledger entries of type allocation created."""
+ if not frappe.db.a_row_exists("Leave Ledger Entry"):
+ return
+
+ duplicate_records_list = get_duplicate_records()
+ delete_duplicate_ledger_entries(duplicate_records_list)
+
+def get_duplicate_records():
+ """Fetch all but one duplicate records from the list of expired leave allocation."""
+ return frappe.db.sql_list("""
+ WITH duplicate_records AS
+ (SELECT
+ name, transaction_name, is_carry_forward,
+ ROW_NUMBER() over(partition by transaction_name order by creation)as row
+ FROM `tabLeave Ledger Entry` l
+ WHERE (EXISTS
+ (SELECT name
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ transaction_name = l.transaction_name
+ AND transaction_type = 'Leave Allocation'
+ AND name <> l.name
+ AND employee = l.employee
+ AND docstatus = 1
+ AND leave_type = l.leave_type
+ AND is_carry_forward=l.is_carry_forward
+ AND to_date = l.to_date
+ AND from_date = l.from_date
+ AND is_expired = 1
+ )))
+ SELECT name FROM duplicate_records WHERE row > 1
+ """)
+
+def delete_duplicate_ledger_entries(duplicate_records_list):
+ """Delete duplicate leave ledger entries."""
+ if duplicate_records_list:
+ frappe.db.sql(''' DELETE FROM `tabLeave Ledger Entry` WHERE name in {0}'''.format(tuple(duplicate_records_list))) #nosec
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/set_serial_no_status.py b/erpnext/patches/v12_0/set_serial_no_status.py
new file mode 100644
index 00000000000..4ec84ef0f9e
--- /dev/null
+++ b/erpnext/patches/v12_0/set_serial_no_status.py
@@ -0,0 +1,17 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import getdate, nowdate
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'serial_no')
+
+ for serial_no in frappe.db.sql("""select name, delivery_document_type, warranty_expiry_date from `tabSerial No`
+ where (status is NULL OR status='')""", as_dict = 1):
+ if serial_no.get("delivery_document_type"):
+ status = "Delivered"
+ elif serial_no.get("warranty_expiry_date") and getdate(serial_no.get("warranty_expiry_date")) <= getdate(nowdate()):
+ status = "Expired"
+ else:
+ status = "Active"
+
+ frappe.db.set_value("Serial No", serial_no.get("name"), "status", status)
\ No newline at end of file
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/unset_customer_supplier_based_on_type_of_item_price.py b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py
new file mode 100644
index 00000000000..60aec05466a
--- /dev/null
+++ b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ invalid_selling_item_price = frappe.db.sql(
+ """SELECT name FROM `tabItem Price` WHERE selling = 1 and buying = 0 and (supplier IS NOT NULL or supplier = '')"""
+ )
+ invalid_buying_item_price = frappe.db.sql(
+ """SELECT name FROM `tabItem Price` WHERE selling = 0 and buying = 1 and (customer IS NOT NULL or customer = '')"""
+ )
+ docs_to_modify = invalid_buying_item_price + invalid_selling_item_price
+ for d in docs_to_modify:
+ # saving the doc will auto reset invalid customer/supplier field
+ doc = frappe.get_doc("Item Price", d[0])
+ doc.save()
\ 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/v12_0/update_price_list_currency_in_bom.py b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py
new file mode 100644
index 00000000000..f5e7b947c23
--- /dev/null
+++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py
@@ -0,0 +1,31 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import getdate, flt
+from erpnext.setup.utils import get_exchange_rate
+
+def execute():
+ frappe.reload_doc("manufacturing", "doctype", "bom")
+ frappe.reload_doc("manufacturing", "doctype", "bom_item")
+
+ frappe.db.sql(""" UPDATE `tabBOM`, `tabPrice List`
+ SET
+ `tabBOM`.price_list_currency = `tabPrice List`.currency,
+ `tabBOM`.plc_conversion_rate = 1.0
+ WHERE
+ `tabBOM`.buying_price_list = `tabPrice List`.name AND `tabBOM`.docstatus < 2
+ AND `tabBOM`.rm_cost_as_per = 'Price List'
+ """)
+
+ for d in frappe.db.sql("""
+ SELECT
+ bom.creation, bom.name, bom.price_list_currency as currency,
+ company.default_currency as company_currency
+ FROM
+ `tabBOM` as bom, `tabCompany` as company
+ WHERE
+ bom.company = company.name AND bom.rm_cost_as_per = 'Price List' AND
+ bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", as_dict=1):
+ plc_conversion_rate = get_exchange_rate(d.currency,
+ d.company_currency, getdate(d.creation), "for_buying")
+
+ frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py
index 179be2cfde5..ec94cd01d1e 100644
--- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py
+++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py
@@ -7,7 +7,7 @@ import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
- if not frappe.db.table_exists("Payroll Period"):
+ if not (frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab")):
return
for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"):
@@ -60,6 +60,9 @@ def execute():
""", (income_tax_slab.name, company.name, period.start_date))
# move other incomes to separate document
+ if not frappe.db.table_exists("Employee Tax Exemption Proof Submission"):
+ return
+
migrated = []
proofs = frappe.get_all("Employee Tax Exemption Proof Submission",
filters = {'docstatus': 1},
@@ -79,6 +82,9 @@ def execute():
except:
pass
+ if not frappe.db.table_exists("Employee Tax Exemption Declaration"):
+ return
+
declerations = frappe.get_all("Employee Tax Exemption Declaration",
filters = {'docstatus': 1},
fields =['payroll_period', 'employee', 'company', 'income_from_other_sources']
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 a326fcc299a..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 : '',
@@ -1375,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]);
}
}
@@ -1396,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
}
},
@@ -1842,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/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/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
index 0849fd7aeb0..3818f566c17 100644
--- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
+++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
@@ -1,10 +1,11 @@
{
- "autoname": "format:MTNG-{date}",
+ "autoname": "naming_series:",
"creation": "2018-10-15 16:25:41.548432",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "naming_series",
"date",
"cb_00",
"status",
@@ -53,9 +54,15 @@
"fieldname": "sb_01",
"fieldtype": "Section Break",
"label": "Minutes"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "MTNG-.YYYY.-.MM.-.DD.-"
}
],
- "modified": "2019-07-13 19:57:40.500541",
+ "modified": "2020-05-19 13:18:59.821740",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Meeting",
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/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py
index 72fe17fb379..203ea8c865a 100644
--- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py
+++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py
@@ -58,7 +58,7 @@ class ImportSupplierInvoice(Document):
"naming_series": self.invoice_series,
"document_type": line.TipoDocumento.text,
"bill_date": get_datetime_str(line.Data.text),
- "invoice_no": line.Numero.text,
+ "bill_no": line.Numero.text,
"total_discount": 0,
"items": [],
"buying_price_list": self.default_buying_price_list
@@ -249,7 +249,7 @@ def create_supplier(supplier_group, args):
return existing_supplier_name
else:
-
+
new_supplier = frappe.new_doc("Supplier")
new_supplier.supplier_name = args.supplier
new_supplier.supplier_group = supplier_group
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 da43cc65553..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)
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 831985fc7cc..d22aa1eebb8 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -236,8 +236,7 @@ def get_tax_template_for_sez(party_details, master_doctype, company, party_type)
def calculate_annual_eligible_hra_exemption(doc):
- basic_component = frappe.get_cached_value('Company', doc.company, "basic_component")
- hra_component = frappe.get_cached_value('Company', doc.company, "hra_component")
+ basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"])
if not (basic_component and hra_component):
frappe.throw(_("Please mention Basic and HRA component in Company"))
annual_exemption, monthly_exemption, hra_amount = 0, 0, 0
@@ -273,7 +272,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 +359,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 +436,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/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 493d8730602..9a5d2ebd910 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -165,6 +165,10 @@ class Customer(TransactionBase):
contact.mobile_no = lead.mobile_no
contact.is_primary_contact = 1
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
+ if lead.email_id:
+ contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1))
+ if lead.mobile_no:
+ contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1))
contact.flags.ignore_permissions = self.flags.ignore_permissions
contact.autoname()
if not frappe.db.exists("Contact", contact.name):
@@ -296,11 +300,15 @@ def get_loyalty_programs(doc):
return lp_details
def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
+ from erpnext.controllers.queries import get_fields
+
if frappe.db.get_default("cust_master_name") == "Customer Name":
fields = ["name", "customer_group", "territory"]
else:
fields = ["name", "customer_name", "customer_group", "territory"]
+ fields = get_fields("Customer", fields)
+
match_conditions = build_match_conditions("Customer")
match_conditions = "and {}".format(match_conditions) if match_conditions else ""
@@ -308,15 +316,18 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
filter_conditions = get_filters_cond(doctype, filters, [])
match_conditions += "{}".format(filter_conditions)
- return frappe.db.sql("""select %s from `tabCustomer` where docstatus < 2
- and (%s like %s or customer_name like %s)
- {match_conditions}
- order by
- case when name like %s then 0 else 1 end,
- case when customer_name like %s then 0 else 1 end,
- name, customer_name limit %s, %s""".format(match_conditions=match_conditions) %
- (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"),
- ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len))
+ return frappe.db.sql("""
+ select %s
+ from `tabCustomer`
+ where docstatus < 2
+ and (%s like %s or customer_name like %s)
+ {match_conditions}
+ order by
+ case when name like %s then 0 else 1 end,
+ case when customer_name like %s then 0 else 1 end,
+ name, customer_name limit %s, %s
+ """.format(match_conditions=match_conditions) % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"),
+ ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len))
def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0):
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/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js
index 85c0cd8bf0c..e3d0a55c3a0 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.js
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.js
@@ -90,6 +90,10 @@ erpnext.SalesFunnel = class SalesFunnel {
get_data(btn) {
var me = this;
+ if (!this.company) {
+ frappe.throw(__("Please Select a Company."));
+ }
+
const method_map = {
"sales_funnel": "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data",
"opp_by_lead_source": "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source",
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py
index d62e2093c69..dba24ef5b00 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.py
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.py
@@ -8,14 +8,23 @@ from frappe import _
from erpnext.accounts.report.utils import convert
import pandas as pd
+def validate_filters(from_date, to_date, company):
+ if from_date and to_date and (from_date >= to_date):
+ frappe.throw(_("To Date must be greater than From Date"))
+
+ if not company:
+ frappe.throw(_("Please Select a Company"))
+
@frappe.whitelist()
def get_funnel_data(from_date, to_date, company):
+ validate_filters(from_date, to_date, company)
+
active_leads = frappe.db.sql("""select count(*) from `tabLead`
where (date(`modified`) between %s and %s)
and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0]
active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact
- left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer'
+ left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer'
and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0]
opportunities = frappe.db.sql("""select count(*) from `tabOpportunity`
@@ -38,6 +47,8 @@ def get_funnel_data(from_date, to_date, company):
@frappe.whitelist()
def get_opp_by_lead_source(from_date, to_date, company):
+ validate_filters(from_date, to_date, company)
+
opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source'])
if opportunities:
@@ -68,11 +79,13 @@ def get_opp_by_lead_source(from_date, to_date, company):
@frappe.whitelist()
def get_pipeline_data(from_date, to_date, company):
+ validate_filters(from_date, to_date, company)
+
opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability'])
if opportunities:
default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency')
-
+
cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities]
df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict()
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/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json
index 71932610a6a..bf9edd6cd49 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.json
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.json
@@ -1,31 +1,31 @@
{
- "add_total_row": 0,
- "creation": "2018-09-21 12:46:29.451048",
- "disable_prepared_report": 0,
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 0,
- "is_standard": "Yes",
- "modified": "2019-05-24 05:37:02.866139",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Sales Analytics",
- "owner": "Administrator",
- "prepared_report": 0,
- "ref_doctype": "Sales Order",
- "report_name": "Sales Analytics",
- "report_type": "Script Report",
+ "add_total_row": 0,
+ "creation": "2018-09-21 12:46:29.451048",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-04-30 19:49:02.303320",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Sales Analytics",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Order",
+ "report_name": "Sales Analytics",
+ "report_type": "Script Report",
"roles": [
{
"role": "Stock User"
- },
+ },
{
"role": "Maintenance User"
- },
+ },
{
"role": "Accounts User"
- },
+ },
{
"role": "Sales Manager"
}
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
index f1726ab8bf7..97d9322918d 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.py
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -194,6 +194,9 @@ class Analytics(object):
def get_rows(self):
self.data = []
self.get_periodic_data()
+ total_row = {
+ "entity": "Total",
+ }
for entity, period_data in iteritems(self.entity_periodic_data):
row = {
@@ -207,6 +210,9 @@ class Analytics(object):
row[scrub(period)] = amount
total += amount
+ if not total_row.get(scrub(period)): total_row[scrub(period)] = 0
+ total_row[scrub(period)] += amount
+
row["total"] = total
if self.filters.tree_type == "Item":
@@ -214,6 +220,8 @@ class Analytics(object):
self.data.append(row)
+ self.data.append(total_row)
+
def get_rows_by_group(self):
self.get_periodic_data()
out = []
@@ -232,8 +240,10 @@ class Analytics(object):
self.entity_periodic_data.setdefault(d.parent, frappe._dict()).setdefault(period, 0.0)
self.entity_periodic_data[d.parent][period] += amount
total += amount
+
row["total"] = total
out = [row] + out
+
self.data = out
def get_periodic_data(self):
diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py
index 4d81a1e4dda..7e8501dcc15 100644
--- a/erpnext/selling/report/sales_analytics/test_analytics.py
+++ b/erpnext/selling/report/sales_analytics/test_analytics.py
@@ -33,6 +33,21 @@ class TestAnalytics(unittest.TestCase):
report = execute(filters)
expected_data = [
+ {
+ 'entity': 'Total',
+ 'apr_2017': 0.0,
+ 'may_2017': 0.0,
+ 'jun_2017': 2000.0,
+ 'jul_2017': 1000.0,
+ 'aug_2017': 0.0,
+ 'sep_2017': 1500.0,
+ 'oct_2017': 1000.0,
+ 'nov_2017': 0.0,
+ 'dec_2017': 0.0,
+ 'jan_2018': 0.0,
+ 'feb_2018': 2000.0,
+ 'mar_2018': 0.0
+ },
{
"entity": "_Test Customer 1",
"entity_name": "_Test Customer 1",
@@ -134,6 +149,21 @@ class TestAnalytics(unittest.TestCase):
report = execute(filters)
expected_data = [
+ {
+ 'entity': 'Total',
+ 'apr_2017': 0.0,
+ 'may_2017': 0.0,
+ 'jun_2017': 20.0,
+ 'jul_2017': 10.0,
+ 'aug_2017': 0.0,
+ 'sep_2017': 15.0,
+ 'oct_2017': 10.0,
+ 'nov_2017': 0.0,
+ 'dec_2017': 0.0,
+ 'jan_2018': 0.0,
+ 'feb_2018': 20.0,
+ 'mar_2018': 0.0
+ },
{
"entity": "_Test Customer 1",
"entity_name": "_Test Customer 1",
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 095b7c3dffa..4a7dd5ad9b4 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -429,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/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index 4c3ac100d5b..8e644f4144d 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -541,27 +541,31 @@ def show_terms(doc):
return doc.tc_name
@frappe.whitelist(allow_guest=True)
-def apply_coupon_code(applied_code,applied_referral_sales_partner):
+def apply_coupon_code(applied_code, applied_referral_sales_partner):
quotation = True
- if applied_code:
- coupon_list=frappe.get_all('Coupon Code', filters={"docstatus": ("<", "2"), 'coupon_code':applied_code }, fields=['name'])
- if coupon_list:
- coupon_name=coupon_list[0].name
- from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
- validate_coupon_code(coupon_name)
- quotation = _get_cart_quotation()
- quotation.coupon_code=coupon_name
+
+ if not applied_code:
+ frappe.throw(_("Please enter a coupon code"))
+
+ coupon_list = frappe.get_all('Coupon Code', filters={'coupon_code': applied_code})
+ if not coupon_list:
+ frappe.throw(_("Please enter a valid coupon code"))
+
+ coupon_name = coupon_list[0].name
+
+ from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
+ validate_coupon_code(coupon_name)
+ quotation = _get_cart_quotation()
+ quotation.coupon_code = coupon_name
+ quotation.flags.ignore_permissions = True
+ quotation.save()
+
+ if applied_referral_sales_partner:
+ sales_partner_list = frappe.get_all('Sales Partner', filters={'referral_code': applied_referral_sales_partner})
+ if sales_partner_list:
+ sales_partner_name = sales_partner_list[0].name
+ quotation.referral_sales_partner = sales_partner_name
quotation.flags.ignore_permissions = True
quotation.save()
- if applied_referral_sales_partner:
- sales_partner_list=frappe.get_all('Sales Partner', filters={'docstatus': 0, 'referral_code':applied_referral_sales_partner }, fields=['name'])
- if sales_partner_list:
- sales_partner_name=sales_partner_list[0].name
- quotation.referral_sales_partner=sales_partner_name
- quotation.flags.ignore_permissions = True
- quotation.save()
- else:
- frappe.throw(_("Please enter valid coupon code !!"))
- else:
- frappe.throw(_("Please enter coupon code !!"))
+
return quotation
diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py
index a7da09cb808..21ee335125b 100644
--- a/erpnext/shopping_cart/product_info.py
+++ b/erpnext/shopping_cart/product_info.py
@@ -10,14 +10,16 @@ from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings
from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status
@frappe.whitelist(allow_guest=True)
-def get_product_info_for_website(item_code):
+def get_product_info_for_website(item_code, skip_quotation_creation=False):
"""get product price / stock info for website"""
cart_settings = get_shopping_cart_settings()
if not cart_settings.enabled:
return frappe._dict()
- cart_quotation = _get_cart_quotation()
+ cart_quotation = frappe._dict()
+ if not skip_quotation_creation:
+ cart_quotation = _get_cart_quotation()
price = get_price(
item_code,
@@ -51,7 +53,7 @@ def get_product_info_for_website(item_code):
def set_product_info_for_website(item):
"""set product price uom for website"""
- product_info = get_product_info_for_website(item.item_code)
+ product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True)
if product_info:
item.update(product_info)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 9b7249e66b9..a091ac7fae9 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.utils import flt, cint
+from frappe.utils import flt, cint, get_link_to_form
from frappe.utils.jinja import render_template
from frappe.utils.data import add_days
from six import string_types
@@ -124,7 +124,7 @@ class Batch(Document):
if has_expiry_date and not self.expiry_date:
frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \
.format(frappe.bold("Shelf Life in Days"),
- frappe.utils.get_link_to_form("Item", self.item),
+ get_link_to_form("Item", self.item),
frappe.bold("Batch Expiry Date")),
title=_("Expiry Date Mandatory"))
@@ -264,16 +264,20 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
cond = ''
- if serial_no:
+ if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'):
+ serial_nos = get_serial_nos(serial_no)
batch = frappe.get_all("Serial No",
fields = ["distinct batch_no"],
filters= {
"item_code": item_code,
"warehouse": warehouse,
- "name": ("in", get_serial_nos(serial_no))
+ "name": ("in", serial_nos)
}
)
+ if not batch:
+ validate_serial_no_with_batch(serial_nos, item_code)
+
if batch and len(batch) > 1:
return []
@@ -288,4 +292,15 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
- """.format(cond), (item_code, warehouse), as_dict=True)
\ No newline at end of file
+ """.format(cond), (item_code, warehouse), as_dict=True)
+
+def validate_serial_no_with_batch(serial_nos, item_code):
+ if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code:
+ frappe.throw(_("The serial no {0} does not belong to item {1}")
+ .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code)))
+
+ serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos])
+
+ message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
+ frappe.throw(_("There is no batch found against the {0}: {1}")
+ .format(message, serial_no_link))
\ No newline at end of file
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..8a2fa397731 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -24,10 +24,10 @@
"return_against",
"customer_po_details",
"po_no",
- "section_break_18",
- "pick_list",
"column_break_17",
"po_date",
+ "section_break_18",
+ "pick_list",
"contact_info",
"shipping_address_name",
"shipping_address",
@@ -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",
@@ -294,7 +294,6 @@
},
{
"collapsible": 1,
- "collapsible_depends_on": "po_no",
"fieldname": "customer_po_details",
"fieldtype": "Section Break",
"label": "Customer PO Details"
@@ -302,7 +301,7 @@
{
"allow_on_submit": 1,
"fieldname": "po_no",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "Customer's Purchase Order No",
"no_copy": 1,
"oldfieldname": "po_no",
@@ -316,7 +315,6 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.po_no",
"fieldname": "po_date",
"fieldtype": "Date",
"label": "Customer's Purchase Order Date",
@@ -324,7 +322,6 @@
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "100px",
- "read_only": 1,
"width": "100px"
},
{
@@ -1240,7 +1237,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2019-12-30 19:17:13.122644",
+ "modified": "2020-05-19 17:03:45.880106",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 3e37a1d4899..f5ffe242a1f 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -467,7 +467,7 @@ class Item(WebsiteGenerator):
def set_shopping_cart_data(self, context):
from erpnext.shopping_cart.product_info import get_product_info_for_website
- context.shopping_cart = get_product_info_for_website(self.name)
+ context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
def add_default_uom_in_conversion_factor_table(self):
uom_conv_list = [d.uom for d in self.get("uoms")]
@@ -572,6 +572,13 @@ class Item(WebsiteGenerator):
frappe.throw(_("Barcode {0} is not a valid {1} code").format(
item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode)
+ if item_barcode.barcode != item_barcode.name:
+ # if barcode is getting updated , the row name has to reset.
+ # Delete previous old row doc and re-enter row as if new to reset name in db.
+ item_barcode.set("__islocal", True)
+ item_barcode.name = None
+ frappe.delete_doc("Item Barcode", item_barcode.name)
+
def validate_warehouse_for_reorder(self):
'''Validate Reorder level table for duplicate and conditional mandatory'''
warehouse = []
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index 957c41546b3..8e39eb5037d 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -69,3 +69,10 @@ class ItemPrice(Document):
self.reference = self.customer
if self.buying:
self.reference = self.supplier
+
+ if self.selling and not self.buying:
+ # if only selling then remove supplier
+ self.supplier = None
+ if self.buying and not self.selling:
+ # if only buying then remove customer
+ self.customer = None
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..c79025c1fb7 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,17 +295,19 @@ 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)
delivery_note.pick_list = pick_list.name
+ delivery_note.customer = pick_list.customer if pick_list.customer else None
return delivery_note
@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 +357,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 53a2fb9b695..2d32999c5c5 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');
@@ -791,39 +791,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/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json
index 48c0f423fd9..6714b2e02c9 100644
--- a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json
+++ b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json
@@ -1,5 +1,5 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
"creation": "2019-09-16 14:10:33.102865",
"disable_prepared_report": 0,
"disabled": 0,
@@ -7,7 +7,7 @@
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
- "modified": "2019-09-21 15:19:55.710578",
+ "modified": "2020-05-13 15:27:45.228418",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Order Items To Be Received or Billed",
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_ledger.py b/erpnext/stock/stock_ledger.py
index 7567a1ae758..ba885c09487 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -527,7 +527,16 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
and cint(erpnext.is_perpetual_inventory_enabled(company)):
frappe.local.message_log = []
- frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.")
- .format(item_code, voucher_type, voucher_no))
+ form_link = frappe.utils.get_link_to_form("Item", item_code)
+
+ message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no)
+ message += "
" + _(" Here are the options to proceed:")
+ solutions = "" + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + ""
+ solutions += "" + _("If not, you can Cancel / Submit this entry ") + _("{0}").format(frappe.bold("after")) + _(" performing either one below:") + ""
+ sub_solutions = "- " + _("Create an incoming stock transaction for the Item.") + "
"
+ sub_solutions += "- " + _("Mention Valuation Rate in the Item master.") + "
"
+ msg = message + solutions + sub_solutions + ""
+
+ frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
return valuation_rate
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/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json
index 0f15e4737fd..1c0c8ff1584 100644
--- a/erpnext/support/web_form/issues/issues.json
+++ b/erpnext/support/web_form/issues/issues.json
@@ -18,7 +18,7 @@
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
- "modified": "2020-03-06 05:24:05.749664",
+ "modified": "2020-05-19 15:08:45.361878",
"modified_by": "Administrator",
"module": "Support",
"name": "issues",
@@ -76,7 +76,7 @@
{
"allow_read_on_all_link_options": 0,
"fieldname": "description",
- "fieldtype": "Text",
+ "fieldtype": "Text Editor",
"hidden": 0,
"label": "Description",
"max_length": 0,
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)