diff --git a/.travis.yml b/.travis.yml index 869fe959c0..a8a0d82614 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,11 @@ dist: trusty python: - "2.7" + - "3.6" + +env: + - TEST_TYPE="Server Side Test" + - TEST_TYPE="Patch Test" services: - mysql @@ -39,18 +44,8 @@ before_script: - bench start & - sleep 10 -jobs: - include: - - stage: test - script: - - set -e - - bench run-tests --app erpnext --coverage - after_script: - - coveralls -b apps/erpnext -d ../../sites/.coverage - env: Server Side Test - - # stage - script: - - wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz - - bench --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz --mariadb-root-password travis - - bench migrate - env: Patch Testing +script: + - bash $TRAVIS_BUILD_DIR/travis/run-tests.sh + +after_script: + - coveralls -b apps/erpnext -d ../../sites/.coverage diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index f2abb81dbb..648cc68dac 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe, json -from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day +from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate from erpnext.accounts.report.general_ledger.general_ledger import execute from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending @@ -37,7 +37,7 @@ def get(chart_name=None, from_date = None, to_date = None): result = build_result(account, dates, gl_entries) return { - "labels": [r[0].strftime('%Y-%m-%d') for r in result], + "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], "datasets": [{ "name": account, "values": [r[1] for r in result] diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js index 2a44cb3b52..0acbe2009f 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js @@ -43,8 +43,13 @@ frappe.ui.form.on('Bank Guarantee', { reference_docname: function(frm) { if (frm.doc.reference_docname && frm.doc.reference_doctype) { - let fields_to_fetch = ["project", "grand_total"]; + let fields_to_fetch = ["grand_total"]; let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier"; + + if (frm.doc.reference_doctype == "Sales Order") { + fields_to_fetch.push("project"); + } + fields_to_fetch.push(party_field); frappe.call({ method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials", diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 8a8e3291aa..5795b8f808 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -232,7 +232,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): if len(pricing_rules) > 1: rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules])) if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage": - pricing_rules = filter(lambda x: x.for_price_list==args.price_list, pricing_rules) \ + pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \ or pricing_rules if len(pricing_rules) > 1 and not args.for_shopping_cart: diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 41205ae2b0..c06856aa75 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -390,8 +390,8 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): if filters: if filters.get("project"): if not isinstance(filters.get("project"), list): - projects = frappe.safe_encode(filters.get("project")) - filters.project = [d.strip() for d in projects.strip().split(',') if d] + filters.project = frappe.parse_json(filters.get("project")) + additional_conditions.append("project in %(project)s") if filters.get("cost_center"): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 4235b7f60d..5c98b249db 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -72,46 +72,25 @@ frappe.query_reports["General Ledger"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "MultiSelect", - get_data: function() { + "fieldtype": "MultiSelectList", + get_data: function(txt) { if (!frappe.query_report.filters) return; - var party_type = frappe.query_report.get_filter_value('party_type'); - var parties = frappe.query_report.get_filter_value('party'); - if(!party_type) return; - const values = parties.split(/\s*,\s*/).filter(d => d); - const txt = parties.match(/[^,\s*]*$/)[0] || ''; - let data = []; + let party_type = frappe.query_report.get_filter_value('party_type'); + if (!party_type) return; - frappe.call({ - type: "GET", - method:'frappe.desk.search.search_link', - async: false, - no_spinner: true, - args: { - doctype: frappe.query_report.get_filter_value('party_type'), - txt: txt, - filters: { - "name": ["not in", values] - } - }, - callback: function(r) { - data = r.results; - } - }); - return data; + return frappe.db.get_link_options(party_type, txt); }, on_change: function() { var party_type = frappe.query_report.get_filter_value('party_type'); var parties = frappe.query_report.get_filter_value('party'); - const values = parties.split(/\s*,\s*/).filter(d => d); - if(!party_type || !parties || values.length>1) { + if(!party_type || parties.length === 0 || parties.length > 1) { frappe.query_report.set_filter_value('party_name', ""); frappe.query_report.set_filter_value('tax_id', ""); return; } else { - var party = values[0]; + var party = parties[0]; var fieldname = erpnext.utils.get_party_name(party_type) || "name"; frappe.db.get_value(party_type, party, fieldname, function(value) { frappe.query_report.set_filter_value('party_name', value[fieldname]); @@ -154,62 +133,17 @@ frappe.query_reports["General Ledger"] = { { "fieldname":"cost_center", "label": __("Cost Center"), - "fieldtype": "MultiSelect", - get_data: function() { - var cost_centers = frappe.query_report.get_filter_value("cost_center") || ""; - - const values = cost_centers.split(/\s*,\s*/).filter(d => d); - const txt = cost_centers.match(/[^,\s*]*$/)[0] || ''; - let data = []; - - frappe.call({ - type: "GET", - method:'frappe.desk.search.search_link', - async: false, - no_spinner: true, - args: { - doctype: "Cost Center", - txt: txt, - filters: { - "company": frappe.query_report.get_filter_value("company"), - "name": ["not in", values] - } - }, - callback: function(r) { - data = r.results; - } - }); - return data; + "fieldtype": "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Cost Center', txt); } }, { "fieldname":"project", "label": __("Project"), - "fieldtype": "MultiSelect", - get_data: function() { - var projects = frappe.query_report.get_filter_value("project") || ""; - - const values = projects.split(/\s*,\s*/).filter(d => d); - const txt = projects.match(/[^,\s*]*$/)[0] || ''; - let data = []; - - frappe.call({ - type: "GET", - method:'frappe.desk.search.search_link', - async: false, - no_spinner: true, - args: { - doctype: "Project", - txt: txt, - filters: { - "name": ["not in", values] - } - }, - callback: function(r) { - data = r.results; - } - }); - return data; + "fieldtype": "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Project', txt); } }, { diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index d9f395b895..1c5e089534 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -26,8 +26,7 @@ def execute(filters=None): account_details.setdefault(acc.name, acc) if filters.get('party'): - parties = cstr(filters.get("party")).strip() - filters.party = [d.strip() for d in parties.split(',') if d] + filters.party = frappe.parse_json(filters.get("party")) validate_filters(filters, account_details) @@ -61,12 +60,10 @@ def validate_filters(filters, account_details): frappe.throw(_("From Date must be before To Date")) if filters.get('project'): - projects = cstr(filters.get("project")).strip() - filters.project = [d.strip() for d in projects.split(',') if d] + filters.project = frappe.parse_json(filters.get('project')) if filters.get('cost_center'): - cost_centers = cstr(filters.get("cost_center")).strip() - filters.cost_center = [d.strip() for d in cost_centers.split(',') if d] + filters.cost_center = frappe.parse_json(filters.get('cost_center')) def validate_party(filters): diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js index 63ac281cdb..8dc5ab36dd 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.js @@ -13,33 +13,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Gross and Net Profit Report"]["filters"].push( { - "fieldname":"project", + "fieldname": "project", "label": __("Project"), - "fieldtype": "MultiSelect", - get_data: function() { - var projects = frappe.query_report.get_filter_value("project") || ""; - - const values = projects.split(/\s*,\s*/).filter(d => d); - const txt = projects.match(/[^,\s*]*$/)[0] || ''; - let data = []; - - frappe.call({ - type: "GET", - method:'frappe.desk.search.search_link', - async: false, - no_spinner: true, - args: { - doctype: "Project", - txt: txt, - filters: { - "name": ["not in", values] - } - }, - callback: function(r) { - data = r.results; - } - }); - return data; + "fieldtype": "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Project', txt); } }, { diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index 250e516d7d..df5c982258 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -8,33 +8,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Profit and Loss Statement"]["filters"].push( { - "fieldname":"project", + "fieldname": "project", "label": __("Project"), - "fieldtype": "MultiSelect", - get_data: function() { - var projects = frappe.query_report.get_filter_value("project") || ""; - - const values = projects.split(/\s*,\s*/).filter(d => d); - const txt = projects.match(/[^,\s*]*$/)[0] || ''; - let data = []; - - frappe.call({ - type: "GET", - method:'frappe.desk.search.search_link', - async: false, - no_spinner: true, - args: { - doctype: "Project", - txt: txt, - filters: { - "name": ["not in", values] - } - }, - callback: function(r) { - data = r.results; - } - }); - return data; + "fieldtype": "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Project', txt); } }, { diff --git a/erpnext/assets/doctype/location/test_location.py b/erpnext/assets/doctype/location/test_location.py index 22d25b5e11..c98b0b0936 100644 --- a/erpnext/assets/doctype/location/test_location.py +++ b/erpnext/assets/doctype/location/test_location.py @@ -25,9 +25,12 @@ class TestLocation(unittest.TestCase): temp['features'][0]['properties']['feature_of'] = location formatted_locations.extend(temp['features']) - formatted_location_string = str(formatted_locations) test_location = frappe.get_doc('Location', 'Test Location Area') test_location.save() - self.assertEqual(formatted_location_string, str(json.loads(test_location.get('location'))['features'])) + test_location_features = json.loads(test_location.get('location'))['features'] + ordered_test_location_features = sorted(test_location_features, key=lambda x: x['properties']['feature_of']) + ordered_formatted_locations = sorted(formatted_locations, key=lambda x: x['properties']['feature_of']) + + self.assertEqual(ordered_formatted_locations, ordered_test_location_features) self.assertEqual(area, test_location.get('area')) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 4d4593641a..adac8f5922 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -342,7 +342,7 @@ def assign_to_user(doc, subject_field): elif doc.lead: assign_user = frappe.db.get_value('Lead', doc.lead, 'lead_owner') - if assign_user and assign_user != 'Administrator': + if assign_user and assign_user not in ['Administrator', 'Guest']: if not assign_to.get(dict(doctype = doc.doctype, name = doc.name)): assign_to.add({ "assign_to": assign_user, diff --git a/erpnext/demo/user/hr.py b/erpnext/demo/user/hr.py index 79f3c19287..0211bc8a90 100644 --- a/erpnext/demo/user/hr.py +++ b/erpnext/demo/user/hr.py @@ -97,7 +97,7 @@ def get_expenses(): "expense_date": frappe.flags.current_date, "expense_type": expense_type.name, "default_account": expense_type.default_account or "Miscellaneous Expenses - WPL", - "claim_amount": claim_amount, + "amount": claim_amount, "sanctioned_amount": claim_amount }) @@ -107,7 +107,7 @@ def update_sanctioned_amount(expense_claim): for expense in expense_claim.expenses: sanctioned_amount = random.randint(1,20)*10 - if sanctioned_amount < expense.claim_amount: + if sanctioned_amount < expense.amount: expense.sanctioned_amount = sanctioned_amount def get_timesheet_based_salary_slip_employee(): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index da0972bf5c..9502006265 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -10,6 +10,7 @@ app_color = "#e74c3c" app_email = "info@erpnext.com" app_license = "GNU General Public License (v3)" source_link = "https://github.com/frappe/erpnext" +app_logo_url = '/assets/erpnext/images/erp-icon.svg' develop_version = '12.x.x-develop' diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js index 376d0c0a5a..22ba5ad473 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.js @@ -2,9 +2,8 @@ frappe.ui.form.on("Employee Attendance Tool", { refresh: function(frm) { frm.disable_save(); }, - + onload: function(frm) { - frm.doc.department = frm.doc.branch = frm.doc.company = "All"; frm.set_value("date", frappe.datetime.get_today()); erpnext.employee_attendance_tool.load_employees(frm); }, @@ -24,7 +23,7 @@ frappe.ui.form.on("Employee Attendance Tool", { company: function(frm) { erpnext.employee_attendance_tool.load_employees(frm); } - + }); diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py index ea5f4bdeca..32fcee1abe 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py @@ -17,12 +17,11 @@ def get_employees(date, department = None, branch = None, company = None): attendance_not_marked = [] attendance_marked = [] filters = {"status": "Active", "date_of_joining": ["<=", date]} - if department != "All": - filters["department"] = department - if branch != "All": - filters["branch"] = branch - if company != "All": - filters["company"] = company + + for field, value in {'department': department, + 'branch': branch, 'company': company}.items(): + if value: + filters[field] = value employee_list = frappe.get_list("Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name") marked_employee = {} diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js index e71ce1276b..b73dcf8ac3 100644 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js @@ -2,20 +2,8 @@ // For license information, please see license.txt frappe.ui.form.on('Employee Benefit Application', { - setup: function(frm) { - if(!frm.doc.employee || !frm.doc.date) { - frappe.throw(__("Please select Employee and Date first")); - } else { - frm.set_query("earning_component", "employee_benefits", function() { - return { - query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", - filters: {date: frm.doc.date, employee: frm.doc.employee} - }; - }); - } - }, - employee: function(frm) { + frm.trigger('set_earning_component'); var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ method = "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; @@ -35,6 +23,21 @@ frappe.ui.form.on('Employee Benefit Application', { get_max_benefits(frm, method, args); } }, + + date: function(frm) { + frm.trigger('set_earning_component'); + }, + + set_earning_component: function(frm) { + if(!frm.doc.employee && !frm.doc.date) return; + frm.set_query("earning_component", "employee_benefits", function() { + return { + query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", + filters: {date: frm.doc.date, employee: frm.doc.employee} + }; + }); + }, + payroll_period: function(frm) { var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 7c6abc7f94..40bec6d4d7 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -109,12 +109,9 @@ cur_frm.cscript.calculate_total = function(doc){ doc.total_claimed_amount = 0; doc.total_sanctioned_amount = 0; $.each((doc.expenses || []), function(i, d) { - doc.total_claimed_amount += d.claim_amount; + doc.total_claimed_amount += d.amount; doc.total_sanctioned_amount += d.sanctioned_amount; }); - - refresh_field("total_claimed_amount"); - refresh_field('total_sanctioned_amount'); }; cur_frm.cscript.calculate_total_amount = function(doc,cdt,cdn){ @@ -157,6 +154,14 @@ frappe.ui.form.on("Expense Claim", { } }; }); + frm.set_query("account_head", "taxes", function(doc) { + return { + filters: [ + ['company', '=', doc.company], + ['account_type', 'in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation"]] + ] + }; + }); }, onload: function(frm) { @@ -197,6 +202,12 @@ frappe.ui.form.on("Expense Claim", { } }, + calculate_grand_total: function(frm) { + var grand_total = flt(frm.doc.total_sanctioned_amount) + flt(frm.doc.total_taxes_and_charges) - flt(frm.doc.total_advance_amount); + frm.set_value("grand_total", grand_total); + frm.refresh_fields(); + }, + make_payment_entry: function(frm) { var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) { @@ -259,6 +270,18 @@ frappe.ui.form.on("Expense Claim", { frm.events.get_advances(frm); }, + get_taxes: function(frm) { + if(frm.doc.taxes) { + frappe.call({ + method: "calculate_taxes", + doc: frm.doc, + callback: () => { + refresh_field("taxes"); + } + }); + } + }, + get_advances: function(frm) { frappe.model.clear_table(frm.doc, "advances"); if (frm.doc.employee) { @@ -288,16 +311,18 @@ frappe.ui.form.on("Expense Claim", { }); frappe.ui.form.on("Expense Claim Detail", { - claim_amount: function(frm, cdt, cdn) { + amount: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; var doc = frm.doc; - frappe.model.set_value(cdt, cdn, 'sanctioned_amount', child.claim_amount); + frappe.model.set_value(cdt, cdn, 'sanctioned_amount', child.amount); cur_frm.cscript.calculate_total(doc,cdt,cdn); }, sanctioned_amount: function(frm, cdt, cdn) { var doc = frm.doc; cur_frm.cscript.calculate_total(doc,cdt,cdn); + frm.trigger("get_taxes"); + frm.trigger("calculate_grand_total"); } }); @@ -324,6 +349,7 @@ frappe.ui.form.on("Expense Claim Advance", { child.advance_paid = r.message[0].paid_amount; child.unclaimed_amount = flt(r.message[0].paid_amount) - flt(r.message[0].claimed_amount); child.allocated_amount = flt(r.message[0].paid_amount) - flt(r.message[0].claimed_amount); + frm.trigger('calculate_grand_total'); refresh_field("advances"); } } @@ -332,6 +358,43 @@ frappe.ui.form.on("Expense Claim Advance", { } }); +frappe.ui.form.on("Expense Taxes and Charges", { + account_head: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if(child.account_head && !child.description) { + // set description from account head + child.description = child.account_head.split(' - ').slice(0, -1).join(' - '); + refresh_field("taxes"); + } + }, + + calculate_total_tax: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + child.total = flt(frm.doc.total_sanctioned_amount) + flt(child.tax_amount); + frm.trigger("calculate_tax_amount", cdt, cdn); + }, + + calculate_tax_amount: function(frm) { + frm.doc.total_taxes_and_charges = 0; + (frm.doc.taxes || []).forEach(function(d) { + frm.doc.total_taxes_and_charges += d.tax_amount; + }); + frm.trigger("calculate_grand_total"); + }, + + rate: function(frm, cdt, cdn) { + var child = locals[cdt][cdn]; + if(!child.amount) { + child.tax_amount = flt(frm.doc.total_sanctioned_amount) * (flt(child.rate)/100); + } + frm.trigger("calculate_total_tax", cdt, cdn); + }, + + tax_amount: function(frm, cdt, cdn) { + frm.trigger("calculate_total_tax", cdt, cdn); + } +}); + cur_frm.fields_dict['task'].get_query = function(doc) { return { filters:{ diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 6e04644036..f0bc268e5d 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -4,6 +4,7 @@ "creation": "2013-01-10 16:34:14", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "naming_series", "employee", @@ -12,17 +13,24 @@ "column_break_5", "expense_approver", "approval_status", - "total_claimed_amount", - "total_sanctioned_amount", "is_paid", "expense_details", "expenses", "sb1", + "taxes", + "transactions_section", + "total_sanctioned_amount", + "total_taxes_and_charges", + "total_advance_amount", + "column_break_17", + "grand_total", + "total_claimed_amount", + "total_amount_reimbursed", + "section_break_16", "posting_date", "vehicle_log", "task", "cb1", - "total_amount_reimbursed", "remark", "title", "email_id", @@ -39,8 +47,7 @@ "status", "amended_from", "advance_payments", - "advances", - "total_advance_amount" + "advances" ], "fields": [ { @@ -117,7 +124,6 @@ { "fieldname": "total_sanctioned_amount", "fieldtype": "Currency", - "in_list_view": 1, "label": "Total Sanctioned Amount", "no_copy": 1, "oldfieldname": "total_sanctioned_amount", @@ -315,12 +321,45 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Expense Taxes and Charges", + "options": "Expense Taxes and Charges" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "transactions_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Grand Total", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_taxes_and_charges", + "fieldtype": "Currency", + "label": "Total Taxes and Charges", + "options": "Company:company:default_currency", + "read_only": 1 } ], "icon": "fa fa-money", "idx": 1, "is_submittable": 1, - "modified": "2019-05-25 22:53:31.682151", + "modified": "2019-06-13 18:05:52.530462", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index d6b0eca70e..caeb2dd946 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -12,6 +12,7 @@ from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account from erpnext.controllers.accounts_controller import AccountsController from frappe.utils.csvutils import getlink +from erpnext.accounts.utils import get_account_currency class InvalidExpenseApproverError(frappe.ValidationError): pass class ExpenseApproverIdentityError(frappe.ValidationError): pass @@ -29,6 +30,7 @@ class ExpenseClaim(AccountsController): self.set_expense_account(validate=True) self.set_payable_account() self.set_cost_center() + self.calculate_taxes() self.set_status() if self.task and not self.project: self.project = frappe.db.get_value("Task", self.task, "project") @@ -93,7 +95,7 @@ class ExpenseClaim(AccountsController): elif self.project: frappe.get_doc("Project", self.project).update_project() - def make_gl_entries(self, cancel = False): + def make_gl_entries(self, cancel=False): if flt(self.total_sanctioned_amount) > 0: gl_entries = self.get_gl_entries() make_gl_entries(gl_entries, cancel) @@ -102,15 +104,13 @@ class ExpenseClaim(AccountsController): gl_entry = [] self.validate_account_details() - payable_amount = flt(self.total_sanctioned_amount) - flt(self.total_advance_amount) - # payable entry - if payable_amount: + if self.grand_total: gl_entry.append( self.get_gl_dict({ "account": self.payable_account, - "credit": payable_amount, - "credit_in_account_currency": payable_amount, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, "against": ",".join([d.default_account for d in self.expenses]), "party_type": "Employee", "party": self.employee, @@ -144,15 +144,16 @@ class ExpenseClaim(AccountsController): "against_voucher": self.name }) ) + self.add_tax_gl_entries(gl_entry) - if self.is_paid and payable_amount: + if self.is_paid and self.grand_total: # payment entry payment_account = get_bank_cash_account(self.mode_of_payment, self.company).get("account") gl_entry.append( self.get_gl_dict({ "account": payment_account, - "credit": payable_amount, - "credit_in_account_currency": payable_amount, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, "against": self.employee }) ) @@ -163,8 +164,8 @@ class ExpenseClaim(AccountsController): "party_type": "Employee", "party": self.employee, "against": payment_account, - "debit": payable_amount, - "debit_in_account_currency": payable_amount, + "debit": self.grand_total, + "debit_in_account_currency": self.grand_total, "against_voucher": self.name, "against_voucher_type": self.doctype, }) @@ -172,6 +173,21 @@ class ExpenseClaim(AccountsController): return gl_entry + def add_tax_gl_entries(self, gl_entries): + # tax table gl entries + for tax in self.get("taxes"): + gl_entries.append( + self.get_gl_dict({ + "account": tax.account_head, + "debit": tax.tax_amount, + "debit_in_account_currency": tax.tax_amount, + "against": self.employee, + "cost_center": self.cost_center, + "against_voucher_type": self.doctype, + "against_voucher": self.name + }) + ) + def validate_account_details(self): if not self.cost_center: frappe.throw(_("Cost center is required to book an expense claim")) @@ -190,9 +206,20 @@ class ExpenseClaim(AccountsController): if self.approval_status == 'Rejected': d.sanctioned_amount = 0.0 - self.total_claimed_amount += flt(d.claim_amount) + self.total_claimed_amount += flt(d.amount) self.total_sanctioned_amount += flt(d.sanctioned_amount) + def calculate_taxes(self): + self.total_taxes_and_charges = 0 + for tax in self.taxes: + if tax.rate: + tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate/100) + + tax.total = flt(tax.tax_amount) + flt(self.total_sanctioned_amount) + self.total_taxes_and_charges += flt(tax.tax_amount) + + self.grand_total = flt(self.total_sanctioned_amount) + flt(self.total_taxes_and_charges) - flt(self.total_advance_amount) + def update_task(self): task = frappe.get_doc("Task", self.task) task.update_total_expense_claim() @@ -224,7 +251,7 @@ class ExpenseClaim(AccountsController): def validate_sanctioned_amount(self): for d in self.get('expenses'): - if flt(d.sanctioned_amount) > flt(d.claim_amount): + if flt(d.sanctioned_amount) > flt(d.amount): frappe.throw(_("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx)) def set_expense_account(self, validate=False): diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.js b/erpnext/hr/doctype/expense_claim/test_expense_claim.js index 070474e10f..d0c43d3be4 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.js @@ -17,7 +17,7 @@ QUnit.test("Test: Expense Claim [HR]", function (assert) { d.expense_date = '2017-08-01', d.expense_type = 'Test Expense Type 1', d.description = 'This is just to test Expense Claim', - d.claim_amount = 2000, + d.amount = 2000, d.sanctioned_amount=2000, refresh_field('expenses'); }, diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 075bc63345..a42209f6ad 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -6,6 +6,7 @@ import frappe import unittest from frappe.utils import random_string, nowdate from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry +from erpnext.accounts.doctype.account.test_account import create_account test_records = frappe.get_test_records('Expense Claim') test_dependencies = ['Employee'] @@ -26,7 +27,7 @@ class TestExpenseClaim(unittest.TestCase): task_name = frappe.db.get_value("Task", {"project": "_Test Project 1"}) payable_account = get_payable_account("Wind Power LLC") - make_expense_claim(payable_account, 300, 200, "Wind Power LLC","Travel Expenses - WP", "_Test Project 1", task_name) + make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP", "_Test Project 1", task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) @@ -62,7 +63,8 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_gl_entry(self): payable_account = get_payable_account("Wind Power LLC") - expense_claim = make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP") + taxes = generate_taxes() + expense_claim = make_expense_claim(payable_account, 300, 200, "Wind Power LLC", "Travel Expenses - WP", do_not_submit=True, taxes=taxes) expense_claim.submit() gl_entries = frappe.db.sql("""select account, debit, credit @@ -72,7 +74,8 @@ class TestExpenseClaim(unittest.TestCase): self.assertTrue(gl_entries) expected_values = dict((d[0], d) for d in [ - [payable_account, 0.0, 200.0], + ['CGST - WP',10.0, 0.0], + [payable_account, 0.0, 210.0], ["Travel Expenses - WP", 200.0, 0.0] ]) @@ -89,7 +92,7 @@ class TestExpenseClaim(unittest.TestCase): "payable_account": payable_account, "approval_status": "Rejected", "expenses": - [{ "expense_type": "Travel", "default_account": "Travel Expenses - WP", "claim_amount": 300, "sanctioned_amount": 200 }] + [{ "expense_type": "Travel", "default_account": "Travel Expenses - WP", "amount": 300, "sanctioned_amount": 200 }] }) expense_claim.submit() @@ -100,22 +103,44 @@ class TestExpenseClaim(unittest.TestCase): self.assertEquals(len(gl_entry), 0) def get_payable_account(company): - return frappe.get_cached_value('Company', company, 'default_payable_account') + return frappe.get_cached_value('Company', company, 'default_payable_account') -def make_expense_claim(payable_account,claim_amount, sanctioned_amount, company, account, project=None, task_name=None): - expense_claim = frappe.get_doc({ +def generate_taxes(): + parent_account = frappe.db.get_value('Account', + {'company': "Wind Power LLC", 'is_group':1, 'account_type': 'Tax'}, + 'name') + account = create_account(company="Wind Power LLC", account_name="CGST", account_type="Tax", parent_account=parent_account) + return {'taxes':[{ + "account_head": account, + "rate": 0, + "description": "CGST", + "tax_amount": 10, + "total": 210 + }]} + +def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): + expense_claim = { "doctype": "Expense Claim", "employee": "_T-Employee-00001", "payable_account": payable_account, "approval_status": "Approved", "company": company, "expenses": - [{ "expense_type": "Travel", "default_account": account, "claim_amount": claim_amount, "sanctioned_amount": sanctioned_amount }] - }) + [{"expense_type": "Travel", + "default_account": account, + "amount": amount, + "sanctioned_amount": sanctioned_amount}]} + if taxes: + expense_claim.update(taxes) + + expense_claim = frappe.get_doc(expense_claim) + if project: expense_claim.project = project if task_name: expense_claim.task = task_name + if do_not_submit: + return expense_claim expense_claim.submit() return expense_claim 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 d4e70575e9..b23fb6af0f 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -253,7 +253,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "claim_amount", + "fieldname": "amount", "fieldtype": "Currency", "hidden": 0, "ignore_user_permissions": 0, @@ -262,7 +262,7 @@ "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, - "label": "Claim Amount", + "label": "Amount", "length": 0, "no_copy": 0, "oldfieldname": "claim_amount", @@ -360,7 +360,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2019-02-24 08:41:36.122565", + "modified": "2019-06-10 08:41:36.122565", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/__init__.py b/erpnext/hr/doctype/expense_taxes_and_charges/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..9bf69daf65 --- /dev/null +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -0,0 +1,105 @@ +{ + "autoname": "hash", + "creation": "2019-06-03 11:42:33.123976", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_head", + "cost_center", + "col_break1", + "rate", + "description", + "section_break_6", + "tax_amount", + "column_break_8", + "total" + ], + "fields": [ + { + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "account_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account Head", + "oldfieldname": "account_head", + "oldfieldtype": "Link", + "options": "Account", + "reqd": 1 + }, + { + "default": ":Company", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "oldfieldname": "cost_center", + "oldfieldtype": "Link", + "options": "Cost Center" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "columns": 2, + "fetch_from": "account_head.tax_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rate", + "oldfieldname": "rate", + "oldfieldtype": "Currency" + }, + { + "columns": 2, + "fieldname": "tax_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "oldfieldname": "tax_amount", + "oldfieldtype": "Currency", + "options": "currency" + }, + { + "columns": 2, + "fieldname": "total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total", + "oldfieldname": "total", + "oldfieldtype": "Currency", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + } + ], + "istable": 1, + "modified": "2019-06-20 12:01:33.919555", + "modified_by": "Administrator", + "module": "HR", + "name": "Expense Taxes and Charges", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.py b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.py new file mode 100644 index 0000000000..4103bef1ff --- /dev/null +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ExpenseTaxesandCharges(Document): + pass diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index ceea4933f3..df633611be 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -18,11 +18,11 @@ class VehicleLog(Document): if (service_detail.service_item or service_detail.type or service_detail.frequency or service_detail.expense_amount): if not (service_detail.service_item and service_detail.type and service_detail.frequency and service_detail.expense_amount): frappe.throw(_("Service Item,Type,frequency and expense amount are required")) - + def on_submit(self): frappe.db.sql("update `tabVehicle` set last_odometer=%s where license_plate=%s", (self.odometer, self.license_plate)) - + @frappe.whitelist() def get_make_model(license_plate): vehicle=frappe.get_doc("Vehicle",license_plate) @@ -41,7 +41,7 @@ def make_expense_claim(docname): for serdetail in vehicle_log.service_detail: total_exp_amt = total_exp_amt + serdetail.expense_amount return total_exp_amt - + vehicle_log = frappe.get_doc("Vehicle Log", docname) exp_claim = frappe.new_doc("Expense Claim") exp_claim.employee=vehicle_log.employee @@ -52,6 +52,6 @@ def make_expense_claim(docname): exp_claim.append("expenses",{ "expense_date":vehicle_log.date, "description":_("Vehicle Expenses"), - "claim_amount":total_claim_amt + "amount":total_claim_amt }) return exp_claim.as_dict() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 42fce5ad49..e9ef70c49b 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -152,7 +152,7 @@ class TestProductionPlan(unittest.TestCase): make_bom(item = item, raw_materials = raw_materials) production_plan = create_production_plan(item_code = 'Production Item CUST') production_plan.make_material_request() - material_request = frappe.get_value('Material Request Item', {'production_plan': production_plan.name}, 'parent') + material_request = frappe.db.get_value('Material Request Item', {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, 'parent') mr = frappe.get_doc('Material Request', material_request) self.assertTrue(mr.material_request_type, 'Customer Provided') self.assertTrue(mr.customer, '_Test Customer') diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 15fd82ea72..747177a3a7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -602,6 +602,6 @@ erpnext.patches.v11_1.set_salary_details_submittable erpnext.patches.v11_1.rename_depends_on_lwp execute:frappe.delete_doc("Report", "Inactive Items") erpnext.patches.v11_1.delete_scheduling_tool -erpnext.patches.v12_0.make_custom_fields_for_bank_remittance +erpnext.patches.v12_0.make_custom_fields_for_bank_remittance #14-06-2019 execute:frappe.delete_doc_if_exists("Page", "support-analytics") erpnext.patches.v12_0.make_item_manufacturer \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index f1179033be..32f0428fcd 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -103,8 +103,8 @@ class TestTimesheet(unittest.TestCase): { "billable": 1, "activity_type": "_Test Activity Type", - "from_type": now_datetime(), - "hours": 3, + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(hours=3), "company": "_Test Company" } ) @@ -113,8 +113,8 @@ class TestTimesheet(unittest.TestCase): { "billable": 1, "activity_type": "_Test Activity Type", - "from_type": now_datetime(), - "hours": 3, + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(hours=3), "company": "_Test Company" } ) diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index 477781bc80..047922f366 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -11,9 +11,6 @@ $(document).bind('toolbar_setup', function() { href="https://discuss.erpnext.com">Feedback
' - $('.navbar-home').html('