[webshop] shopping cart settings, update price list, taxes and shipping rule on change of address, apply defaults on creation of fresh quotation

This commit is contained in:
Anand Doshi
2013-07-04 17:13:53 +05:30
parent 259e0b978c
commit 99100a4135
40 changed files with 1225 additions and 474 deletions

View File

@@ -9,42 +9,67 @@ from webnotes.model.controller import DocListController
class ShoppingCartSetupError(webnotes.ValidationError): pass
class DocType(DocListController):
def __init__(self, d, dl):
self.doc, self.doclist = d, dl
def validate(self):
if self.doc.enabled:
self.validate_overlapping_territories("shopping_cart_price_lists", "price_list")
self.validate_overlapping_territories("shopping_cart_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
self.validate_shipping_rules()
self.validate_price_lists()
self.validate_tax_masters()
self.validate_exchange_rates_exist()
def validate_overlapping_territories(self, parentfield, fieldname):
names = [doc.fields(fieldname) for doc in self.doclist.get({"parentfield": parentfield})]
doctype = self.meta.get_field(parentfield).options
parenttype = self.meta.get_field(fieldname, parentfield=parentfield).options
if not names:
msgprint(_("Please specify at least one") + " " + _(doctype),
raise_exception=ShoppingCartSetupError)
territory_name = webnotes.conn.sql("""select territory, parent from `tabFor Territory`
where parenttype=%s and parent in (%s)""" % ("%s", ", ".join(["%s"]*names)),
tuple([parenttype] + names))
territory_name_map = {}
for territory, name in territory_name:
territory_name_map.setdefault(territory, []).append(name)
def validate_overlapping_territories(self, parentfield, fieldname):
# for displaying message
doctype = self.meta.get_field(parentfield).options
# specify atleast one entry in the table
self.validate_table_has_rows(parentfield, raise_exception=ShoppingCartSetupError)
territory_name_map = self.get_territory_name_map(parentfield, fieldname)
for territory, names in territory_name_map.items():
if len(names) > 1:
msgprint(_("Error for") + " " + _(doctype) + ": " + comma_and(names) +
" " + _("have a common territory") + ": " + territory,
raise_exception=ShoppingCartSetupError)
def validate_shipping_rules(self):
pass
return territory_name_map
def validate_price_lists(self):
territory_name_map = self.validate_overlapping_territories("price_lists",
"price_list")
# validate that a Shopping Cart Price List exists for the root territory
# as a catch all!
from setup.utils import get_root_of
root_territory = get_root_of("Territory")
if root_territory not in territory_name_map.keys():
msgprint(_("Please specify a Price List which is valid for Territory") +
": " + root_territory, raise_exception=ShoppingCartSetupError)
def validate_tax_masters(self):
self.validate_overlapping_territories("sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
def get_territory_name_map(self, parentfield, fieldname):
territory_name_map = {}
# entries in table
names = [doc.fields.get(fieldname) for doc in self.doclist.get({"parentfield": parentfield})]
if names:
# for condition in territory check
parenttype = self.meta.get_field(fieldname, parentfield=parentfield).options
# to validate territory overlap
# make a map of territory: [list of names]
# if list against each territory has more than one element, raise exception
territory_name = webnotes.conn.sql("""select `territory`, `parent`
from `tabFor Territory`
where `parenttype`=%s and `parent` in (%s) """ %
("%s", ", ".join(["%s"]*len(names))), tuple([parenttype] + names))
for territory, name in territory_name:
territory_name_map.setdefault(territory, []).append(name)
return territory_name_map
def validate_exchange_rates_exist(self):
"""check if exchange rates exist for all Price List currencies (to company's currency)"""
@@ -54,7 +79,7 @@ class DocType(DocListController):
raise_exception=ShoppingCartSetupError)
price_list_currency_map = webnotes.conn.get_values("Price List",
[d.price_list for d in self.doclist.get({"parentfield": "shopping_cart_price_lists"})],
[d.price_list for d in self.doclist.get({"parentfield": "price_lists"})],
"currency")
expected_to_exist = [currency + "-" + company_currency
@@ -70,5 +95,39 @@ class DocType(DocListController):
if missing:
msgprint(_("Missing Currency Exchange Rates for" + ": " + comma_and(missing)),
raise_exception=ShoppingCartSetupError)
def get_name_from_territory(self, territory, parentfield, fieldname):
name = None
territory_name_map = self.get_territory_name_map(parentfield, fieldname)
if territory_name_map.get(territory):
name = territory_name_map.get(territory)[0]
else:
territory_ancestry = self.get_territory_ancestry(territory)
for ancestor in territory_ancestry:
if territory_name_map.get(ancestor):
name = territory_name_map.get(ancestor)[0]
break
return name
def get_price_list(self, billing_territory):
return self.get_name_from_territory(billing_territory, "price_lists", "price_list")
def get_tax_master(self, billing_territory):
return self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
def get_shipping_rule(self, shipping_territory):
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
def get_territory_ancestry(self, territory):
from setup.utils import get_ancestors_of
if not hasattr(self, "_territory_ancestry"):
self._territory_ancestry = {}
if not self._territory_ancestry.get(territory):
self._territory_ancestry[territory] = get_ancestors_of("Territory", territory)
return self._territory_ancestry[territory]

View File

@@ -2,7 +2,7 @@
{
"creation": "2013-06-19 15:57:32",
"docstatus": 0,
"modified": "2013-06-21 12:59:30",
"modified": "2013-07-03 21:00:00",
"modified_by": "Administrator",
"owner": "Administrator"
},
@@ -53,7 +53,7 @@
},
{
"doctype": "DocField",
"fieldname": "shopping_cart_price_lists",
"fieldname": "price_lists",
"fieldtype": "Table",
"label": "Shopping Cart Price Lists",
"options": "Shopping Cart Price List",
@@ -61,12 +61,20 @@
},
{
"doctype": "DocField",
"fieldname": "shopping_cart_taxes_and_charges_masters",
"fieldname": "sales_taxes_and_charges_masters",
"fieldtype": "Table",
"label": "Shopping Cart Taxes and Charges Masters",
"options": "Shopping Cart Taxes and Charges Master",
"reqd": 0
},
{
"doctype": "DocField",
"fieldname": "shopping_cart_shipping_rules",
"fieldtype": "Table",
"label": "Shopping Cart Shipping Rules",
"options": "Shopping Cart Shipping Rule",
"reqd": 0
},
{
"doctype": "DocField",
"fieldname": "company",

View File

@@ -0,0 +1,78 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import webnotes
import unittest
from website.doctype.shopping_cart_settings.shopping_cart_settings import ShoppingCartSetupError
class TestShoppingCartSettings(unittest.TestCase):
def setUp(self):
webnotes.conn.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
webnotes.conn.sql("""delete from `tabShopping Cart Price List`""")
webnotes.conn.sql("""delete from `tabShopping Cart Taxes and Charges Master`""")
webnotes.conn.sql("""delete from `tabShopping Cart Shipping Rule`""")
def get_cart_settings(self):
return webnotes.bean({"doctype": "Shopping Cart Settings",
"company": "_Test Company"})
def test_price_list_territory_overlap(self):
cart_settings = self.get_cart_settings()
def _add_price_list(price_list):
cart_settings.doclist.append({
"doctype": "Shopping Cart Price List",
"parentfield": "price_lists",
"price_list": price_list
})
for price_list in ("_Test Price List Rest of the World", "_Test Price List India",
"_Test Price List"):
_add_price_list(price_list)
controller = cart_settings.make_controller()
controller.validate_overlapping_territories("price_lists", "price_list")
_add_price_list("_Test Price List 2")
controller = cart_settings.make_controller()
self.assertRaises(ShoppingCartSetupError, controller.validate_overlapping_territories,
"price_lists", "price_list")
return cart_settings
def test_taxes_territory_overlap(self):
cart_settings = self.get_cart_settings()
def _add_tax_master(tax_master):
cart_settings.doclist.append({
"doctype": "Shopping Cart Taxes and Charges Master",
"parentfield": "sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master": tax_master
})
for tax_master in ("_Test Sales Taxes and Charges Master", "_Test India Tax Master"):
_add_tax_master(tax_master)
controller = cart_settings.make_controller()
controller.validate_overlapping_territories("sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
_add_tax_master("_Test Sales Taxes and Charges Master 2")
controller = cart_settings.make_controller()
self.assertRaises(ShoppingCartSetupError, controller.validate_overlapping_territories,
"sales_taxes_and_charges_masters", "sales_taxes_and_charges_master")
def test_exchange_rate_exists(self):
webnotes.conn.sql("""delete from `tabCurrency Exchange`""")
cart_settings = self.test_price_list_territory_overlap()
controller = cart_settings.make_controller()
self.assertRaises(ShoppingCartSetupError, controller.validate_exchange_rates_exist)
from setup.doctype.currency_exchange.test_currency_exchange import test_records as \
currency_exchange_records
webnotes.bean(currency_exchange_records[0]).insert()
controller.validate_exchange_rates_exist()

View File

@@ -0,0 +1,8 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import webnotes
class DocType:
def __init__(self, d, dl):
self.doc, self.doclist = d, dl

View File

@@ -0,0 +1,35 @@
[
{
"creation": "2013-07-03 13:15:34",
"docstatus": 0,
"modified": "2013-07-03 13:19:02",
"modified_by": "Administrator",
"owner": "Administrator"
},
{
"doctype": "DocType",
"istable": 1,
"module": "Website",
"name": "__common__"
},
{
"doctype": "DocField",
"fieldname": "shipping_rule",
"fieldtype": "Link",
"label": "Shipping Rule",
"name": "__common__",
"options": "Shipping Rule",
"parent": "Shopping Cart Shipping Rule",
"parentfield": "fields",
"parenttype": "DocType",
"permlevel": 0,
"reqd": 1
},
{
"doctype": "DocType",
"name": "Shopping Cart Shipping Rule"
},
{
"doctype": "DocField"
}
]

View File

@@ -69,6 +69,8 @@ def update_cart_address(address_fieldname, address_name):
quotation.ignore_permissions = True
quotation.save()
apply_cart_settings(quotation=quotation)
return get_cart_quotation(quotation.doclist)
@@ -131,14 +133,23 @@ def get_lead_or_customer():
"doctype": "Lead",
"email_id": webnotes.session.user,
"lead_name": get_fullname(webnotes.session.user),
"territory": webnotes.conn.get_value("Shopping Cart Settings", None, "territory") or \
"All Territories",
"territory": guess_territory(),
"status": "Open" # TODO: set something better???
})
lead_bean.ignore_permissions = True
lead_bean.insert()
return lead_bean.doc
def guess_territory():
territory = None
geoip_country = webnotes.session.get("session_country")
if geoip_country:
territory = webnotes.conn.get_value("Territory", geoip_country)
return territory or \
webnotes.conn.get_value("Shopping Cart Settings", None, "territory") or \
"All Territories"
def decorate_quotation_doclist(doclist):
for d in doclist:
@@ -168,42 +179,84 @@ def _get_cart_quotation(party=None):
"order_type": "Shopping Cart",
"status": "Draft",
"__islocal": 1,
"price_list_name": get_price_list(party),
(party.doctype.lower()): party.name
})
qbean.run_method("onload_post_render")
apply_cart_settings(party, qbean)
return qbean
def get_price_list(party):
if not party.default_price_list:
party.default_price_list = get_price_list_using_geoip()
party.save()
return party.default_price_list
def get_price_list_using_geoip():
country = webnotes.session.get("session_country")
price_list_name = None
if country:
price_list_name = webnotes.conn.sql("""select parent
from `tabPrice List Country` plc
where country=%s and exists (select name from `tabPrice List` pl
where use_for_website=1 and ifnull(valid_for_all_countries, 0)=0 and
pl.name = plc.parent)""", country)
def apply_cart_settings(party=None, quotation=None):
if not party:
party = get_lead_or_customer()
if not quotation:
quotation = _get_cart_quotation(party)
if price_list_name:
price_list_name = price_list_name[0][0]
else:
price_list_name = webnotes.conn.get_value("Price List",
{"use_for_website": 1, "valid_for_all_countries": 1})
if not price_list_name:
raise WebsitePriceListMissingError, "No website Price List specified"
cart_settings = webnotes.get_obj("Shopping Cart Settings")
return price_list_name
billing_territory = get_address_territory(quotation.doc.customer_address) or \
party.territory
set_price_list_and_rate(quotation, cart_settings, billing_territory)
set_taxes(quotation, cart_settings, billing_territory)
# set shipping rule based on shipping territory
shipping_territory = get_address_territory(quotation.doc.shipping_address_name) or \
party.territory
apply_shipping_rule(quotation, cart_settings, shipping_territory)
quotation.run_method("calculate_taxes_and_totals")
quotation.save()
def set_price_list_and_rate(quotation, cart_settings, billing_territory):
"""set price list based on billing territory"""
quotation.doc.price_list_name = cart_settings.get_price_list(billing_territory)
# reset values
quotation.doc.price_list_currency = quotation.doc.currency = \
quotation.doc.plc_conversion_rate = quotation.doc.conversion_rate = None
for item in quotation.doclist.get({"parentfield": "quotation_details"}):
item.ref_rate = item.adj_rate = item.export_rate = item.export_amount = None
# refetch values
quotation.run_method("set_price_list_and_item_details")
def set_taxes(quotation, cart_settings, billing_territory):
"""set taxes based on billing territory"""
quotation.doc.charge = cart_settings.get_tax_master(billing_territory)
# clear table
quotation.doclist = quotation.doc.clear_table(quotation.doclist, "other_charges")
# append taxes
controller = quotation.make_controller()
controller.append_taxes_from_master("other_charges", "charge")
quotation.set_doclist(controller.doclist)
def apply_shipping_rule(quotation, cart_settings, shipping_territory):
quotation.doc.shipping_rule = cart_settings.get_shipping_rule(shipping_territory)
quotation.run_method("apply_shipping_rule")
def get_address_territory(address_name):
"""Tries to match city, state and country of address to existing territory"""
territory = None
if address_name:
address_fields = webnotes.conn.get_value("Address", address_name,
["city", "state", "country"])
for value in address_fields:
territory = webnotes.conn.get_value("Territory", value)
if territory:
break
return territory
def get_cart_price_list(territory_list, territory_name_map):
pass
@webnotes.whitelist()
def checkout():
quotation = _get_cart_quotation()