diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 53fba5b7d2b..6a54164ea5f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -989,4 +989,4 @@ def make_sales_return(source_name, target_doc=None): def set_account_for_mode_of_payment(self): for data in self.payments: if not data.account: - data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") + data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") \ No newline at end of file diff --git a/erpnext/domains/retail.py b/erpnext/domains/retail.py index 1bfd65faf82..897c4d9e401 100644 --- a/erpnext/domains/retail.py +++ b/erpnext/domains/retail.py @@ -10,8 +10,7 @@ data = { 'ToDo' ], 'properties': [ - {'doctype': 'Item', 'fieldname': 'manufacturing', 'property': 'hidden', 'value': 1}, - {'doctype': 'Customer', 'fieldname': 'credit_limit_section', 'property': 'hidden', 'value': 1}, + {'doctype': 'Item', 'fieldname': 'manufacturing', 'property': 'hidden', 'value': 1} ], 'set_value': [ ['Stock Settings', None, 'show_barcode_field', 1] diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 222d2e569eb..fd9d0a173fe 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -797,7 +797,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Credit Limit", + "label": "Credit Limit and Payment Terms", "length": 0, "no_copy": 0, "permlevel": 0, @@ -810,7 +810,7 @@ "search_index": 0, "set_only_once": 0, "unique": 0, - "width": "50%" + "width": "" }, { "allow_bulk_edit": 0, @@ -876,6 +876,37 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "bypass_credit_limit_check_at_sales_order", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Bypass credit limit check at Sales Order", + "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 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1171,8 +1202,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-08-31 15:12:18.637132", - "modified_by": "tundebabzy@gmail.com", + "modified": "2017-11-23 17:41:23.243421", + "modified_by": "Administrator", "module": "Selling", "name": "Customer", "name_case": "Title Case", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 64cd190d210..a24f4a368e3 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -170,23 +170,30 @@ def check_credit_limit(customer, company): throw(_("Please contact to the user who have Sales Master Manager {0} role") .format(" / " + credit_controller if credit_controller else "")) -def get_customer_outstanding(customer, company): +def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False): # Outstanding based on GL Entries - outstanding_based_on_gle = frappe.db.sql("""select sum(debit) - sum(credit) - from `tabGL Entry` where party_type = 'Customer' and party = %s and company=%s""", (customer, company)) + outstanding_based_on_gle = frappe.db.sql(""" + select sum(debit) - sum(credit) + from `tabGL Entry` + where party_type = 'Customer' and party = %s and company=%s""", (customer, company)) outstanding_based_on_gle = flt(outstanding_based_on_gle[0][0]) if outstanding_based_on_gle else 0 # Outstanding based on Sales Order - outstanding_based_on_so = frappe.db.sql(""" - select sum(base_grand_total*(100 - per_billed)/100) - from `tabSales Order` - where customer=%s and docstatus = 1 and company=%s - and per_billed < 100 and status != 'Closed'""", (customer, company)) + outstanding_based_on_so = 0.0 - outstanding_based_on_so = flt(outstanding_based_on_so[0][0]) if outstanding_based_on_so else 0.0 + # if credit limit check is bypassed at sales order level, + # we should not consider outstanding Sales Orders, when customer credit balance report is run + if not ignore_outstanding_sales_order: + outstanding_based_on_so = frappe.db.sql(""" + select sum(base_grand_total*(100 - per_billed)/100) + from `tabSales Order` + where customer=%s and docstatus = 1 and company=%s + and per_billed < 100 and status != 'Closed'""", (customer, company)) - # Outstanding based on Delivery Note + outstanding_based_on_so = flt(outstanding_based_on_so[0][0]) if outstanding_based_on_so else 0.0 + + # Outstanding based on Delivery Note, which are not created against Sales Order unmarked_delivery_note_items = frappe.db.sql("""select dn_item.name, dn_item.amount, dn.base_net_total, dn.base_grand_total from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item @@ -215,7 +222,8 @@ def get_credit_limit(customer, company): credit_limit = None if customer: - credit_limit, customer_group = frappe.db.get_value("Customer", customer, ["credit_limit", "customer_group"]) + credit_limit, customer_group = frappe.db.get_value("Customer", + customer, ["credit_limit", "customer_group"]) if not credit_limit: credit_limit = frappe.db.get_value("Customer Group", customer_group, "credit_limit") @@ -223,4 +231,4 @@ def get_credit_limit(customer, company): if not credit_limit: credit_limit = frappe.db.get_value("Company", company, "credit_limit") - return flt(credit_limit) + return flt(credit_limit) \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b3563c3c3ae..c9e77336cd9 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -14,6 +14,7 @@ from frappe.desk.notifications import clear_doctype_notifications from frappe.contacts.doctype.address.address import get_company_address from erpnext.controllers.selling_controller import SellingController from erpnext.accounts.doctype.subscription.subscription import get_next_schedule_date +from erpnext.selling.doctype.customer.customer import check_credit_limit form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -188,49 +189,68 @@ class SalesOrder(SellingController): def update_project(self): project_list = [] if self.project: - project = frappe.get_doc("Project", self.project) - project.flags.dont_sync_tasks = True - project.update_sales_costing() - project.save() - project_list.append(self.project) + project = frappe.get_doc("Project", self.project) + project.flags.dont_sync_tasks = True + project.update_sales_costing() + project.save() + project_list.append(self.project) def check_credit_limit(self): - from erpnext.selling.doctype.customer.customer import check_credit_limit - check_credit_limit(self.customer, self.company) + # if bypass credit limit check is set to true (1) at sales order level, + # then we need not to check credit limit and vise versa + if not cint(frappe.db.get_value("Customer", self.customer, "bypass_credit_limit_check_at_sales_order")): + check_credit_limit(self.customer, self.company) def check_nextdoc_docstatus(self): # Checks Delivery Note - submit_dn = frappe.db.sql_list("""select t1.name from `tabDelivery Note` t1,`tabDelivery Note Item` t2 + submit_dn = frappe.db.sql_list(""" + select t1.name + from `tabDelivery Note` t1,`tabDelivery Note Item` t2 where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", self.name) + if submit_dn: - frappe.throw(_("Delivery Notes {0} must be cancelled before cancelling this Sales Order").format(comma_and(submit_dn))) + frappe.throw(_("Delivery Notes {0} must be cancelled before cancelling this Sales Order") + .format(comma_and(submit_dn))) # Checks Sales Invoice submit_rv = frappe.db.sql_list("""select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", self.name) + if submit_rv: - frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order").format(comma_and(submit_rv))) + frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order") + .format(comma_and(submit_rv))) #check maintenance schedule - submit_ms = frappe.db.sql_list("""select t1.name from `tabMaintenance Schedule` t1, - `tabMaintenance Schedule Item` t2 + submit_ms = frappe.db.sql_list(""" + select t1.name + from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", self.name) + if submit_ms: - frappe.throw(_("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order").format(comma_and(submit_ms))) + frappe.throw(_("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order") + .format(comma_and(submit_ms))) # check maintenance visit - submit_mv = frappe.db.sql_list("""select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 + submit_mv = frappe.db.sql_list(""" + select t1.name + from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""",self.name) + if submit_mv: - frappe.throw(_("Maintenance Visit {0} must be cancelled before cancelling this Sales Order").format(comma_and(submit_mv))) + frappe.throw(_("Maintenance Visit {0} must be cancelled before cancelling this Sales Order") + .format(comma_and(submit_mv))) # check production order - pro_order = frappe.db.sql_list("""select name from `tabProduction Order` + pro_order = frappe.db.sql_list(""" + select name + from `tabProduction Order` where sales_order = %s and docstatus = 1""", self.name) + if pro_order: - frappe.throw(_("Production Order {0} must be cancelled before cancelling this Sales Order").format(comma_and(pro_order))) + frappe.throw(_("Production Order {0} must be cancelled before cancelling this Sales Order") + .format(comma_and(pro_order))) def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") @@ -464,6 +484,11 @@ def make_delivery_note(source_name, target_doc=None): else: target.po_no = source.po_no + # Since the credit limit check is bypassed at sales order level, + # we need to check it at delivery note + if cint(frappe.db.get_value("Customer", source.customer, "bypass_credit_limit_check_at_sales_order")): + check_credit_limit(source.customer, source.company) + target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -528,6 +553,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") + # Since the credit limit check is bypassed at sales order level, we need to check it at sales invoice + if cint(frappe.db.get_value("Customer", source.customer, "bypass_credit_limit_check_at_sales_order")): + check_credit_limit(source.customer, source.company) + # set company address target.update(get_company_address(target.company)) if target.company_address: @@ -791,4 +820,4 @@ def get_default_bom_item(item_code): order_by='is_default desc') bom = bom[0].name if bom else None - return bom + return bom \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_bypass_credit_limit_check.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_bypass_credit_limit_check.js new file mode 100644 index 00000000000..3ffb8257178 --- /dev/null +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_bypass_credit_limit_check.js @@ -0,0 +1,54 @@ +QUnit.module('Sales Order'); + +QUnit.test("test_sales_order_with_bypass_credit_limit_check", function(assert) { +//#PR : 10861, Author : ashish-greycube & jigneshpshah, Email:mr.ashish.shah@gmail.com + assert.expect(2); + let done = assert.async(); + frappe.run_serially([ + () => frappe.new_doc('Customer'), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("customer_name", "Test Customer 10"), + () => cur_frm.set_value("credit_limit", 100.00), + () => cur_frm.set_value("bypass_credit_limit_check_at_sales_order", 1), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + + () => frappe.new_doc('Item'), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("item_code", "Test Product 10"), + () => cur_frm.set_value("item_group", "Products"), + () => cur_frm.set_value("standard_rate", 100), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + + () => { + return frappe.tests.make('Sales Order', [ + {customer: 'Test Customer 5'}, + {items: [ + [ + {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)}, + {'qty': 5}, + {'item_code': 'Test Product 10'}, + ] + ]} + + ]); + }, + () => cur_frm.save(), + () => frappe.tests.click_button('Submit'), + () => assert.equal("Confirm", cur_dialog.title,'confirmation for submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(3), + () => { + + assert.ok(cur_frm.doc.status=="To Deliver and Bill", "It is submited. Credit limit is NOT checked for sales order"); + + + }, + () => done() + ]); +}); diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_without_bypass_credit_limit_check.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_without_bypass_credit_limit_check.js new file mode 100644 index 00000000000..ea15edc0e1f --- /dev/null +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_without_bypass_credit_limit_check.js @@ -0,0 +1,59 @@ +QUnit.module('Sales Order'); + +QUnit.test("test_sales_order_without_bypass_credit_limit_check", function(assert) { +//#PR : 10861, Author : ashish-greycube & jigneshpshah, Email:mr.ashish.shah@gmail.com + assert.expect(2); + let done = assert.async(); + frappe.run_serially([ + () => frappe.new_doc('Customer'), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("customer_name", "Test Customer 11"), + () => cur_frm.set_value("credit_limit", 100.00), + () => cur_frm.set_value("bypass_credit_limit_check_at_sales_order", 0), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + + () => frappe.new_doc('Item'), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("item_code", "Test Product 11"), + () => cur_frm.set_value("item_group", "Products"), + () => cur_frm.set_value("standard_rate", 100), + // save form + () => cur_frm.save(), + () => frappe.timeout(1), + + () => { + return frappe.tests.make('Sales Order', [ + {customer: 'Test Customer 11'}, + {items: [ + [ + {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)}, + {'qty': 5}, + {'item_code': 'Test Product 11'}, + ] + ]} + + ]); + }, + () => cur_frm.save(), + () => frappe.tests.click_button('Submit'), + () => assert.equal("Confirm", cur_dialog.title,'confirmation for submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(3), + () => { + + if (cur_dialog.body.innerText.match(/^Credit limit has been crossed for customer.*$/)) + { + /*Match found */ + assert.ok(true, "Credit Limit crossed message received"); + } + + + }, + () => cur_dialog.cancel(), + () => done() + ]); +}); diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index 9075c3fa9f9..ffa418017cd 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -19,14 +19,19 @@ def execute(filters=None): for d in customer_list: row = [] - outstanding_amt = get_customer_outstanding(d.name, filters.get("company")) - credit_limit = get_credit_limit(d.name, filters.get("company")) + + outstanding_amt = get_customer_outstanding(d.name, filters.get("company"), + ignore_outstanding_sales_order=d.bypass_credit_limit_check_at_sales_order) + + credit_limit = get_credit_limit(d.name, filters.get("company")) + bal = flt(credit_limit) - flt(outstanding_amt) if customer_naming_type == "Naming Series": - row = [d.name, d.customer_name, credit_limit, outstanding_amt, bal] + row = [d.name, d.customer_name, credit_limit, outstanding_amt, bal, + d.bypass_credit_limit_check_at_sales_order] else: - row = [d.name, credit_limit, outstanding_amt, bal] + row = [d.name, credit_limit, outstanding_amt, bal, d.bypass_credit_limit_check_at_sales_order] if credit_limit: data.append(row) @@ -35,8 +40,11 @@ def execute(filters=None): def get_columns(customer_naming_type): columns = [ - _("Customer") + ":Link/Customer:120", _("Credit Limit") + ":Currency:120", - _("Outstanding Amt") + ":Currency:100", _("Credit Balance") + ":Currency:120" + _("Customer") + ":Link/Customer:120", + _("Credit Limit") + ":Currency:120", + _("Outstanding Amt") + ":Currency:100", + _("Credit Balance") + ":Currency:120", + _("Bypass credit check at Sales Order ") + ":Check:240" ] if customer_naming_type == "Naming Series": @@ -50,5 +58,6 @@ def get_details(filters): if filters.get("customer"): conditions += " where name = %(customer)s" - return frappe.db.sql("""select name, customer_name from `tabCustomer` %s""" - % conditions, filters, as_dict=1) + return frappe.db.sql("""select name, customer_name, + bypass_credit_limit_check_at_sales_order from `tabCustomer` %s + """ % conditions, filters, as_dict=1) \ No newline at end of file