Currency filter in Pricing Rule (#11776)

* added currency filter

* modified files

* pull margin only if currency matches

* Renamed price to rate in pricing rule

* fetch rate only if currency matches

* rebase with develop

* rebase with develop

* patch to set currency in existing docs

* currency field mandatory in pricing rule

* modified test cases

* rebase with develop

* fixed test case
This commit is contained in:
Shreya Shah 2018-02-20 11:26:46 +05:30 committed by Nabin Hait
parent 9b74f8bd37
commit f718b0c0df
13 changed files with 261 additions and 51 deletions

View File

@ -14,7 +14,7 @@ frappe.ui.form.on("Pricing Rule", "refresh", function(frm) {
${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}
</li>
<li>
${__("If selected Pricing Rule is made for 'Price', it will overwrite Price List. Pricing Rule price is the final price, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}
${__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}
</li>
<li>
${__('Discount Percentage can be applied either against a Price List or for all Price List.')}
@ -97,8 +97,8 @@ cur_frm.cscript.margin_type = function(doc){
cur_frm.set_df_property('margin_rate_or_amount', 'description', doc.margin_type=='Percentage'?'In Percentage %':'In Amount')
}
frappe.ui.form.on('Pricing Rule', 'price_or_discount', function(frm){
if(frm.doc.price_or_discount == 'Price') {
frappe.ui.form.on('Pricing Rule', 'rate_or_discount', function(frm){
if(frm.doc.rate_or_discount == 'Rate') {
frm.set_value('for_price_list', "")
}
})

View File

@ -910,6 +910,38 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "currency",
"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": "Currency",
"length": 0,
"no_copy": 0,
"options": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -1070,7 +1102,7 @@
"collapsible": 0,
"columns": 0,
"default": "Discount Percentage",
"fieldname": "price_or_discount",
"fieldname": "rate_or_discount",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
@ -1079,10 +1111,10 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Price or Discount",
"label": "Rate or Discount",
"length": 0,
"no_copy": 0,
"options": "\nPrice\nDiscount Percentage",
"options": "\nRate\nDiscount Percentage",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -1128,8 +1160,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.price_or_discount==\"Price\"",
"fieldname": "price",
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
@ -1138,7 +1170,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Price",
"label": "Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -1158,7 +1190,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.price_or_discount==\"Discount Percentage\"",
"depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"",
"fieldname": "discount_percentage",
"fieldtype": "Float",
"hidden": 0,
@ -1188,7 +1220,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.price_or_discount==\"Discount Percentage\"",
"depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"",
"fieldname": "for_price_list",
"fieldtype": "Link",
"hidden": 0,
@ -1284,7 +1316,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-09-27 08:31:38.432574",
"modified": "2018-02-12 17:21:22.559996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@ -21,7 +21,7 @@ class PricingRule(Document):
self.validate_applicable_for_selling_or_buying()
self.validate_min_max_qty()
self.cleanup_fields_value()
self.validate_price_or_discount()
self.validate_rate_or_discount()
self.validate_max_discount()
if not self.margin_type: self.margin_rate_or_amount = 0.0
@ -50,7 +50,7 @@ class PricingRule(Document):
throw(_("Min Qty can not be greater than Max Qty"))
def cleanup_fields_value(self):
for logic_field in ["apply_on", "applicable_for", "price_or_discount"]:
for logic_field in ["apply_on", "applicable_for", "rate_or_discount"]:
fieldname = frappe.scrub(self.get(logic_field) or "")
# reset all values except for the logic field
@ -62,13 +62,13 @@ class PricingRule(Document):
if f!=fieldname:
self.set(f, None)
def validate_price_or_discount(self):
for field in ["Price"]:
def validate_rate_or_discount(self):
for field in ["Rate"]:
if flt(self.get(frappe.scrub(field))) < 0:
throw(_("{0} can not be negative").format(field))
def validate_max_discount(self):
if self.price_or_discount == "Discount Percentage" and self.item_code:
if self.rate_or_discount == "Discount Percentage" and self.item_code:
max_discount = frappe.db.get_value("Item", self.item_code, "max_discount")
if max_discount and flt(self.discount_percentage) > flt(max_discount):
throw(_("Max discount allowed for item: {0} is {1}%").format(self.item_code, max_discount))
@ -176,17 +176,34 @@ def get_pricing_rule_for_item(args):
if pricing_rule:
item_details.pricing_rule = pricing_rule.name
item_details.pricing_rule_for = pricing_rule.price_or_discount
item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
if pricing_rule.price_or_discount == "Price":
item_details.update({
"price_list_rate": (pricing_rule.price/flt(args.conversion_rate)) * args.conversion_factor or 1.0 \
if args.conversion_rate else 0.0,
"discount_percentage": 0.0
})
item_details.pricing_rule_for = pricing_rule.rate_or_discount
if pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency:
item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
elif pricing_rule.margin_type == 'Percentage':
item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
else:
item_details.margin_type = None
item_details.margin_rate_or_amount = 0.0
if pricing_rule.rate_or_discount == 'Rate':
if pricing_rule.currency == args.currency:
item_details.update({
"price_list_rate": pricing_rule.rate,
"discount_percentage": 0.0
})
else:
item_details.update({
"price_list_rate": 0.0,
"discount_percentage": 0.0
})
else:
item_details.discount_percentage = pricing_rule.discount_percentage or args.discount_percentage
elif args.get('pricing_rule'):
item_details = remove_pricing_rule_for_item(args.get("pricing_rule"), item_details)
@ -194,8 +211,8 @@ def get_pricing_rule_for_item(args):
def remove_pricing_rule_for_item(pricing_rule, item_details):
pricing_rule = frappe.db.get_value('Pricing Rule', pricing_rule,
['price_or_discount', 'margin_type'], as_dict=1)
if pricing_rule and pricing_rule.price_or_discount == 'Discount Percentage':
['rate_or_discount', 'margin_type'], as_dict=1)
if pricing_rule and pricing_rule.rate_or_discount == 'Discount Percentage':
item_details.discount_percentage = 0.0
if pricing_rule and pricing_rule.margin_type in ['Percentage', 'Amount']:
@ -318,8 +335,8 @@ def filter_pricing_rules(args, pricing_rules):
break
if len(pricing_rules) > 1:
price_or_discount = list(set([d.price_or_discount for d in pricing_rules]))
if len(price_or_discount) == 1 and price_or_discount[0] == "Discount Percentage":
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) \
or pricing_rules

View File

@ -22,9 +22,10 @@ class TestPricingRule(unittest.TestCase):
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"item_code": "_Test Item",
"currency": "USD",
"selling": 1,
"price_or_discount": "Discount Percentage",
"price": 0,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 10,
"company": "_Test Company"
}
@ -102,8 +103,9 @@ class TestPricingRule(unittest.TestCase):
"apply_on": "Item Code",
"item_code": "_Test FG Item 2",
"selling": 1,
"price_or_discount": "Discount Percentage",
"price": 0,
"currency": "USD",
"rate_or_discount": "Discount Percentage",
"rate": 0,
"margin_type": "Percentage",
"margin_rate_or_amount": 10,
"company": "_Test Company"
@ -167,10 +169,11 @@ class TestPricingRule(unittest.TestCase):
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule 1",
"apply_on": "Item Code",
"currency": "USD",
"item_code": "_Test Variant Item",
"selling": 1,
"price_or_discount": "Discount Percentage",
"price": 0,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 7.5,
"company": "_Test Company"
}).insert()
@ -198,9 +201,10 @@ class TestPricingRule(unittest.TestCase):
"title": "_Test Pricing Rule 2",
"apply_on": "Item Code",
"item_code": "Test Variant PRT",
"currency": "USD",
"selling": 1,
"price_or_discount": "Discount Percentage",
"price": 0,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 17.5,
"company": "_Test Company"
}).insert()
@ -215,10 +219,11 @@ class TestPricingRule(unittest.TestCase):
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"item_code": "_Test Item",
"selling": 1,
"price_or_discount": "Discount Percentage",
"price": 0,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 5,
"max_qty": 7,
"discount_percentage": 17.5,
@ -283,12 +288,13 @@ def make_pricing_rule(**args):
"item_code": args.item_code or "_Test Item",
"applicable_for": args.applicable_for,
"selling": args.selling or 0,
"currency": "USD",
"buying": args.buying or 0,
"min_qty": args.min_qty or 0.0,
"max_qty": args.max_qty or 0.0,
"price_or_discount": args.price_or_discount or "Discount Percentage",
"rate_or_discount": args.rate_or_discount or "Discount Percentage",
"discount_percentage": args.discount_percentage or 0.0,
"price": args.price or 0.0,
"rate": args.rate or 0.0,
"margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0
}).insert(ignore_permissions=True)

View File

@ -1,4 +1,4 @@
QUnit.module('Pricing Rule"');
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule", function(assert) {
assert.expect(2);
@ -11,6 +11,7 @@ QUnit.test("test pricing rule", function(assert) {
{selling:1},
{applicable_for:'Customer'},
{customer:'Test Customer 3'},
{currency: frappe.defaults.get_default("currency")}
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},

View File

@ -0,0 +1,58 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule with different currency", function(assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule 2'},
{apply_on: 'Item Code'},
{item_code:'Test Product 4'},
{selling:1},
{priority: 1},
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{margin_type: 'Amount'},
{margin_rate_or_amount: 20},
{rate_or_discount: 'Rate'},
{rate:200},
{currency:'USD'}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 4');
},
() => {
return frappe.tests.make('Sales Order', [
{customer: 'Test Customer 1'},
{currency: 'INR'},
{items: [
[
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 5},
{'item_code': "Test Product 4"}
]
]}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 2', "Pricing rule correct");
// margin not applied because different currency in pricing rule
assert.ok(cur_frm.doc.items[0].margin_type==null, "Margin correct");
},
() => frappe.timeout(0.3),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -0,0 +1,56 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule with same currency", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule 1'},
{apply_on: 'Item Code'},
{item_code:'Test Product 4'},
{selling:1},
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{rate_or_discount: 'Rate'},
{rate:200},
{currency:'USD'}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 4');
},
() => {
return frappe.tests.make('Sales Order', [
{customer: 'Test Customer 1'},
{currency: 'USD'},
{items: [
[
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 5},
{'item_code': "Test Product 4"}
]
]}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 1', "Pricing rule correct");
assert.ok(cur_frm.doc.items[0].price_list_rate==200, "Item rate correct");
// get_total
assert.ok(cur_frm.doc.total== 1000, "Total correct");
},
() => frappe.timeout(0.3),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -510,13 +510,22 @@ class calculate_taxes_and_totals(object):
self.doc.precision("base_write_off_amount"))
def calculate_margin(self, item):
rate_with_margin = 0.0
base_rate_with_margin = 0.0
if item.price_list_rate:
if item.pricing_rule and not self.doc.ignore_pricing_rule:
pricing_rule = frappe.get_doc('Pricing Rule', item.pricing_rule)
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
if pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency:
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
elif pricing_rule.margin_type == 'Percentage':
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
else:
item.margin_type = None
item.margin_rate_or_amount = 0.0
if item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100

View File

@ -21,7 +21,7 @@ Pricing Rule master has two sections:
### 1. Applicability Section:
In this section, conditions are set for the application of Pricing Rule. When transaction meets condition as specified in this section, Price or Discount as specified in the Pricing Rule will be applied. You can set condition on following values.
In this section, conditions are set for the application of Pricing Rule. When transaction meets condition as specified in this section, Rate or Discount as specified in the Pricing Rule will be applied. You can set condition on following values.
####1.1 Applicable On:
@ -43,15 +43,16 @@ Specify minimum and maximum qty of an item when this Pricing Rule should be appl
###2. Application:
Using Price List Rule, you can ultimately define price or %discount to be applied on an item.
Using Price List Rule, you can ultimately define rate or %discount to be applied on an item.
<img alt="Applicable" class="screenshot" src="{{docs_base_url}}/assets/img/articles/pricing-rule-application.png">
####2.1 Price
####2.1 Rate
Price or Discount specified in the Pricing Rule will be applied only if above applicability rules are matched with values in the transaction. Price mentioned in Pricing Rule will be given priority over item's Price List rate.
Rate or Discount specified in the Pricing Rule will be applied only if above applicability rules are matched with values in the transaction. Rate mentioned in Pricing Rule will be given priority over item's Price List rate.
<img alt="Applicable Price" class="screenshot" src="{{docs_base_url}}/assets/img/articles/pricing-rule-price.png">
<img alt="Applicable Rate" class="screenshot" src="/docs/assets/img/articles/pricing-rule-price.png">
>>>>>>> Renamed price to rate in pricing rule
#### 2.2 Discount Percentage

View File

@ -494,6 +494,8 @@ erpnext.patches.v10_0.set_default_payment_terms_based_on_company
erpnext.patches.v10_0.update_sales_order_link_to_purchase_order
erpnext.patches.v10_0.added_extra_gst_custom_field_in_gstr2 #2018-02-13
erpnext.patches.v10_0.item_barcode_childtable_migrate
erpnext.patches.v10_0.rename_price_to_rate_in_pricing_rule
erpnext.patches.v10_0.set_currency_in_pricing_rule
erpnext.patches.v10_0.workflow_expense_claim
erpnext.patches.v10_0.set_b2c_limit
erpnext.patches.v10_0.update_translatable_fields

View File

@ -0,0 +1,14 @@
from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
frappe.reload_doc("accounts", "doctype", "pricing_rule")
try:
rename_field("Pricing Rule", "price_or_discount", "rate_or_discount")
rename_field("Pricing Rule", "price", "rate")
except Exception as e:
if e.args[0]!=1054:
raise

View File

@ -0,0 +1,12 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doctype("Pricing Rule")
currency = frappe.db.get_default("currency")
for doc in frappe.get_all('Pricing Rule', fields = ["company", "name"]):
if doc.company:
currency = frappe.db.get_value("Company", doc.company, "default_currency")
frappe.db.sql("""update `tabPricing Rule` set currency = %s where name = %s""",(currency, doc.name))

View File

@ -2,7 +2,7 @@ erpnext/tests/ui/make_fixtures.js #long
erpnext/accounts/doctype/account/tests/test_account.js
erpnext/accounts/doctype/account/tests/test_make_tax_account.js
erpnext/accounts/doctype/account/tests/test_account_with_number.js
erpnext/accounts/doctype/pricing_rule/test_pricing_rule.js
erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule.js
erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.js
erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.js
erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js
@ -106,3 +106,5 @@ erpnext/selling/doctype/quotation/tests/test_quotation_with_margin.js
erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_margin.js
erpnext/hr/doctype/payroll_entry/test_set_salary_components.js
erpnext/hr/doctype/payroll_entry/test_payroll_entry.js
erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_same_currency.js
erpnext/accounts/doctype/pricing_rule/tests/test_pricing_rule_with_different_currency.js