From 615d3420563012782d1845c4d2dd4a99538d26fa Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 25 Feb 2014 19:01:20 +0530 Subject: [PATCH] Pricing Rule: first commit --- .../accounts/doctype/pricing_rule/__init__.py | 0 .../doctype/pricing_rule/pricing_rule.py | 36 +++ .../doctype/pricing_rule/pricing_rule.txt | 270 ++++++++++++++++++ .../doctype/pricing_rule/test_pricing_rule.py | 68 +++++ erpnext/stock/doctype/item/test_item.py | 2 +- erpnext/stock/get_item_details.py | 80 +++++- 6 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 erpnext/accounts/doctype/pricing_rule/__init__.py create mode 100644 erpnext/accounts/doctype/pricing_rule/pricing_rule.py create mode 100644 erpnext/accounts/doctype/pricing_rule/pricing_rule.txt create mode 100644 erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py diff --git a/erpnext/accounts/doctype/pricing_rule/__init__.py b/erpnext/accounts/doctype/pricing_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py new file mode 100644 index 0000000000..6a2a6ef8aa --- /dev/null +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -0,0 +1,36 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import throw, _ + +class DocType: + def __init__(self, d, dl): + self.doc, self.doclist = d, dl + + def validate(self): + self.validate_mandatory() + self.cleanup_fields_value() + + def validate_mandatory(self): + for field in ["apply_on", "applicable_for", "price_or_discount"]: + val = self.doc.fields.get("applicable_for") + if val and not self.doc.fields.get(frappe.scrub(val)): + throw("{fname} {msg}".format(fname = _(val), msg = _(" is mandatory")), + frappe.MandatoryError) + + def cleanup_fields_value(self): + fields = ["item_code", "item_group", "brand", "customer", "customer_group", "territory", + "supplier", "supplier_type", "campaign", "sales_partner", "price", "discount"] + + for field_with_value in ["apply_on", "applicable_for", "price_or_discount"]: + val = self.doc.fields.get(field_with_value) + if val: + fields.remove(frappe.scrub(val)) + + for field in fields: + self.doc.fields[field] = None + \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.txt b/erpnext/accounts/doctype/pricing_rule/pricing_rule.txt new file mode 100644 index 0000000000..bf8e892577 --- /dev/null +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.txt @@ -0,0 +1,270 @@ +[ + { + "creation": "2014-02-21 15:02:51", + "docstatus": 0, + "modified": "2014-02-25 13:59:13", + "modified_by": "Administrator", + "owner": "Administrator" + }, + { + "autoname": "PRULE.#####", + "doctype": "DocType", + "document_type": "Master", + "istable": 0, + "module": "Accounts", + "name": "__common__" + }, + { + "doctype": "DocField", + "name": "__common__", + "parent": "Pricing Rule", + "parentfield": "fields", + "parenttype": "DocType", + "permlevel": 0 + }, + { + "create": 1, + "doctype": "DocPerm", + "name": "__common__", + "parent": "Pricing Rule", + "parentfield": "permissions", + "parenttype": "DocType", + "permlevel": 0, + "read": 1, + "write": 1 + }, + { + "doctype": "DocType", + "name": "Pricing Rule" + }, + { + "doctype": "DocField", + "fieldname": "basic_section", + "fieldtype": "Section Break", + "label": "Basic Section" + }, + { + "default": "Item Code", + "doctype": "DocField", + "fieldname": "apply_on", + "fieldtype": "Select", + "label": "Apply On", + "options": "\nItem Code\nItem Group\nBrand", + "reqd": 1 + }, + { + "depends_on": "eval:doc.apply_on==\"Item Code\"", + "doctype": "DocField", + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item Code", + "options": "Item", + "reqd": 0 + }, + { + "depends_on": "eval:doc.apply_on==\"Item Group\"", + "doctype": "DocField", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "depends_on": "eval:doc.apply_on==\"Brand\"", + "doctype": "DocField", + "fieldname": "brand", + "fieldtype": "Link", + "label": "Brand", + "options": "Brand" + }, + { + "doctype": "DocField", + "fieldname": "priority", + "fieldtype": "Select", + "label": "Priority", + "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20" + }, + { + "doctype": "DocField", + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "doctype": "DocField", + "fieldname": "valid_from", + "fieldtype": "Date", + "label": "Valid From" + }, + { + "doctype": "DocField", + "fieldname": "valid_upto", + "fieldtype": "Date", + "label": "Valid Upto" + }, + { + "doctype": "DocField", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + }, + { + "doctype": "DocField", + "fieldname": "price_discount_section", + "fieldtype": "Section Break", + "label": "Price / Discount" + }, + { + "default": "Discount", + "doctype": "DocField", + "fieldname": "price_or_discount", + "fieldtype": "Select", + "label": "Price or Discount", + "options": "\nPrice\nDiscount", + "reqd": 1 + }, + { + "doctype": "DocField", + "fieldname": "col_break2", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.price_or_discount==\"Price\"", + "doctype": "DocField", + "fieldname": "price", + "fieldtype": "Float", + "label": "Price" + }, + { + "depends_on": "eval:doc.price_or_discount==\"Discount\"", + "doctype": "DocField", + "fieldname": "discount", + "fieldtype": "Float", + "label": "Discount" + }, + { + "depends_on": "eval:doc.price_or_discount==\"Discount\"", + "doctype": "DocField", + "fieldname": "for_price_list", + "fieldtype": "Link", + "label": "For Price List", + "options": "Price List" + }, + { + "doctype": "DocField", + "fieldname": "applicability_section", + "fieldtype": "Section Break", + "label": "Applicability" + }, + { + "doctype": "DocField", + "fieldname": "applicable_for", + "fieldtype": "Select", + "label": "Applicable For", + "options": "\nCustomer\nCustomer Group\nTerritory\nSales Person\nSales Partner\nCampaign\nSupplier\nSupplier Type" + }, + { + "doctype": "DocField", + "fieldname": "col_break3", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.applicable_for==\"Customer\"", + "doctype": "DocField", + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "depends_on": "eval:doc.applicable_for==\"Customer Group\"", + "doctype": "DocField", + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, + { + "depends_on": "eval:doc.applicable_for==\"Territory\"", + "doctype": "DocField", + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "options": "Territory" + }, + { + "depends_on": "eval:doc.applicable_for==\"Sales Partner\"", + "doctype": "DocField", + "fieldname": "sales_partner", + "fieldtype": "Link", + "label": "Sales Partner", + "options": "Sales Partner" + }, + { + "depends_on": "eval:doc.applicable_for==\"Campaign\"", + "doctype": "DocField", + "fieldname": "campaign", + "fieldtype": "Link", + "label": "Campaign", + "options": "Campaign" + }, + { + "depends_on": "eval:doc.applicable_for==\"Supplier\"", + "doctype": "DocField", + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "depends_on": "eval:doc.applicable_for==\"Supplier Type\"", + "doctype": "DocField", + "fieldname": "supplier_type", + "fieldtype": "Link", + "label": "Supplier Type", + "options": "Supplier Type" + }, + { + "doctype": "DocField", + "fieldname": "qty_section", + "fieldtype": "Section Break", + "label": "Based on Qty" + }, + { + "doctype": "DocField", + "fieldname": "min_qty", + "fieldtype": "Float", + "label": "Min Qty" + }, + { + "doctype": "DocField", + "fieldname": "col_break4", + "fieldtype": "Column Break" + }, + { + "doctype": "DocField", + "fieldname": "max_qty", + "fieldtype": "Float", + "label": "Max Qty" + }, + { + "doctype": "DocPerm", + "role": "Accounts Manager" + }, + { + "doctype": "DocPerm", + "role": "Sales Manager" + }, + { + "doctype": "DocPerm", + "role": "Purchase Manager" + }, + { + "doctype": "DocPerm", + "role": "Website Manager" + }, + { + "doctype": "DocPerm", + "role": "System Manager" + } +] \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py new file mode 100644 index 0000000000..d09902ccdd --- /dev/null +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -0,0 +1,68 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from __future__ import unicode_literals +import unittest +import frappe + +class TestPricingRule(unittest.TestCase): + def test_pricing_rule_for_discount(self): + from erpnext.stock.get_item_details import get_item_details + from frappe import MandatoryError + + args = frappe._dict({ + "item_code": "_Test Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "transaction_type": "selling", + "customer": "_Test Customer", + }) + + details = get_item_details(args) + self.assertEquals(details.get("discount_percentage"), 10) + + prule = frappe.bean(copy=test_records[0]) + prule.doc.apply_on = "Item Group" + prule.doc.item_group = "_Test Item Group" + prule.doc.discount = 15 + prule.insert() + + details = get_item_details(args) + self.assertEquals(details.get("discount_percentage"), 10) + + prule = frappe.bean(copy=test_records[0]) + prule.doc.applicable_for = "Customer" + self.assertRaises(MandatoryError, prule.insert) + prule.doc.customer = "_Test Customer" + prule.doc.discount = 20 + prule.insert() + details = get_item_details(args) + self.assertEquals(details.get("discount_percentage"), 20) + + args.item_code = "_Test Item 2" + details = get_item_details(args) + self.assertEquals(details.get("discount_percentage"), 15) + + args.customer = None + details = get_item_details(args) + self.assertEquals(details.get("discount_percentage"), 15) + + +test_records = [ + [{ + "doctype": "Pricing Rule", + "apply_on": "Item Code", + "item_code": "_Test Item", + "price_or_discount": "Discount", + "price": 0, + "discount": 10, + }], + +] \ No newline at end of file diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index f990ec90ef..25a0c71860 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -18,7 +18,7 @@ class TestItem(unittest.TestCase): item.doc.default_warehouse = None self.assertRaises(WarehouseNotSet, item.insert) - def atest_get_item_details(self): + def test_get_item_details(self): from erpnext.stock.get_item_details import get_item_details to_check = { "item_code": "_Test Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index dfe4cc36be..79613c3a45 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -18,7 +18,7 @@ def get_item_details(args): "conversion_rate": 1.0, "selling_price_list": None, "price_list_currency": None, - "plc_conversion_rate": 1.0 + "plc_conversion_rate": 1.0, "doctype": "", "docname": "", "supplier": None, @@ -60,11 +60,11 @@ def get_item_details(args): out.update(get_projected_qty(item.name, out.warehouse)) get_price_list_rate(args, item_bean, out) - - out.update(get_item_discount(out.item_group, args.customer)) if args.transaction_type == "selling" and cint(args.is_pos): out.update(get_pos_settings_item_details(args.company, args)) + + apply_pricing_rule(out, args) if args.get("doctype") in ("Sales Invoice", "Delivery Note"): if item_bean.doc.has_serial_no == "Yes" and not args.serial_no: @@ -262,6 +262,80 @@ def get_pos_settings(company): return pos_settings and pos_settings[0] or None +def apply_pricing_rule(out, args): + args_dict = frappe._dict().update(args) + args_dict.update(out) + + for rule_for in ["price", "discount"]: + pricing_rules = get_pricing_rules(args_dict, rule_for) + if pricing_rules: + if rule_for == "discount": + out["discount_percentage"] = pricing_rules[-1]["discount"] + else: + out["base_price_list_rate"] = pricing_rules[0]["price"] + out["price_list_rate"] = pricing_rules[0]["price"] * \ + flt(args_dict.plc_conversion_rate) / flt(args_dict.conversion_rate) + + +def get_pricing_rules(args_dict, price_or_discount): + def _filter_pricing_rule(pricing_rules, field_set): + p_rules = [] + for field in field_set: + if not p_rules: + for p_rule in pricing_rules: + if p_rule[field] == args_dict.get(field): + p_rules.append(p_rule) + else: + break + + return p_rules or pricing_rules + + pricing_rules = get_all_pricing_rules(args_dict, price_or_discount) + + for field_set in [["item_code", "item_group", "brand"], ["customer", "customer_group", + "territory", "supplier", "supplier_type", "campaign", "sales_partner"]]: + if pricing_rules: + pricing_rules = _filter_pricing_rule(pricing_rules, field_set) + + # filter for price list + if pricing_rules: + pricing_rules = filter(lambda x: (not x.for_price_list or + x.for_price_list==args_dict.price_list), pricing_rules) + + # filter for qty + if pricing_rules and args_dict.get("qty"): + pricing_rules = filter(lambda x: (args_dict.qty>=flt(x.min_qty) + and (args_dict.qty<=x.max_qty if x.max_qty else True)), pricing_rules) + + # find pricing rule with highest priority + if pricing_rules: + max_priority = min([cint(p.priority) for p in pricing_rules]) + if max_priority: + pricing_rules = filter(lambda x: x.priority==max_priority, pricing_rules) + + if len(pricing_rules) > 1: + pricing_rules = sorted(pricing_rules, key=lambda x: x[price_or_discount]) + + return pricing_rules + +def get_all_pricing_rules(args_dict, price_or_discount): + conditions = " and ifnull(%s, 0) > 0" % price_or_discount + + for field in ["customer", "customer_group", "territory", "supplier", "supplier_type", + "campaign", "sales_partner"]: + if args_dict.get(field): + conditions += " and ifnull("+field+", '') in (%("+field+")s, '')" + + if args_dict.get("transaction_date"): + conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') + and ifnull(valid_upto, '2500-12-31')""" + + return frappe.conn.sql("""select * from `tabPricing Rule` + where (item_code=%(item_code)s or item_group=%(item_group)s or brand=%(brand)s) + and docstatus < 2 and ifnull(disable, 0) = 0 {0} + order by priority desc, name desc""".format(conditions), args_dict, as_dict=1) + + def get_serial_nos_by_fifo(args, item_bean): return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and status='Available'