[shopping-cart] i'm back

This commit is contained in:
Rushabh Mehta
2014-10-21 16:16:30 +05:30
parent 39dbf73de7
commit 3daa49ac1f
82 changed files with 4047 additions and 10 deletions

View File

@@ -0,0 +1,126 @@
# 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 frappe
from frappe import _
from frappe.utils import get_fullname, flt
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import is_shopping_cart_enabled, get_default_territory
# TODO
# validate stock of each item in Website Warehouse or have a list of possible warehouses in Shopping Cart Settings
def get_quotation(user=None):
if not user:
user = frappe.session.user
if user == "Guest":
raise frappe.PermissionError
is_shopping_cart_enabled()
party = get_party(user)
values = {
"order_type": "Shopping Cart",
party.doctype.lower(): party.name,
"docstatus": 0,
"contact_email": user
}
try:
quotation = frappe.get_doc("Quotation", values)
except frappe.DoesNotExistError:
quotation = frappe.new_doc("Quotation")
quotation.update(values)
if party.doctype == "Customer":
quotation.contact_person = frappe.db.get_value("Contact", {"customer": party.name, "email_id": user})
quotation.insert(ignore_permissions=True)
return quotation
def set_item_in_cart(item_code, qty, user=None):
validate_item(item_code)
quotation = get_quotation(user=user)
qty = flt(qty)
quotation_item = quotation.get("quotation_details", {"item_code": item_code})
if qty==0:
if quotation_item:
# remove
quotation.get("quotation_details").remove(quotation_item[0])
else:
# add or update
if quotation_item:
quotation_item[0].qty = qty
else:
quotation.append("quotation_details", {
"doctype": "Quotation Item",
"item_code": item_code,
"qty": qty
})
quotation.save(ignore_permissions=True)
return quotation
def set_address_in_cart(address_fieldname, address, user=None):
quotation = get_quotation(user=user)
validate_address(quotation, address_fieldname, address)
if quotation.get(address_fieldname) != address:
quotation.set(address_fieldname, address)
if address_fieldname=="customer_address":
quotation.set("address_display", None)
else:
quotation.set("shipping_address", None)
quotation.save(ignore_permissions=True)
return quotation
def validate_item(item_code):
item = frappe.db.get_value("Item", item_code, ["item_name", "show_in_website"], as_dict=True)
if not item.show_in_website:
frappe.throw(_("{0} cannot be purchased using Shopping Cart").format(item.item_name))
def validate_address(quotation, address_fieldname, address):
party = get_party(quotation.contact_email)
address_doc = frappe.get_doc(address)
if address_doc.get(party.doctype.lower()) != party.name:
if address_fieldname=="customer_address":
frappe.throw(_("Invalid Billing Address"))
else:
frappe.throw(_("Invalid Shipping Address"))
def get_party(user):
def _get_party(user):
customer = frappe.db.get_value("Contact", {"email_id": user}, "customer")
if customer:
return frappe.get_doc("Customer", customer)
lead = frappe.db.get_value("Lead", {"email_id": user})
if lead:
return frappe.get_doc("Lead", lead)
# create a lead
lead = frappe.new_doc("Lead")
lead.update({
"email_id": user,
"lead_name": get_fullname(user),
"territory": guess_territory()
})
lead.insert(ignore_permissions=True)
return lead
if not getattr(frappe.local, "shopping_cart_party", None):
frappe.local.shopping_cart_party = {}
if not frappe.local.shopping_cart_party.get(user):
frappe.local.shopping_cart_party[user] = _get_party(user)
return frappe.local.shopping_cart_party[user]
def guess_territory():
territory = None
if frappe.session.get("session_country"):
territory = frappe.db.get_value("Territory", frappe.session.get("session_country"))
return territory or get_default_territory()

View File

@@ -0,0 +1,434 @@
# 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 frappe
from frappe import throw, _
import frappe.defaults
from frappe.utils import flt, get_fullname, fmt_money, cstr
from erpnext.utilities.doctype.address.address import get_address_display
from frappe.utils.nestedset import get_root_of
class WebsitePriceListMissingError(frappe.ValidationError): pass
def set_cart_count(quotation=None):
if not quotation:
quotation = _get_cart_quotation()
cart_count = cstr(len(quotation.get("quotation_details")))
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
@frappe.whitelist()
def get_cart_quotation(doc=None):
party = get_lead_or_customer()
if not doc:
quotation = _get_cart_quotation(party)
doc = quotation
set_cart_count(quotation)
return {
"doc": decorate_quotation_doc(doc),
"addresses": [{"name": address.name, "display": address.display}
for address in get_address_docs(party)],
"shipping_rules": get_applicable_shipping_rules(party)
}
@frappe.whitelist()
def place_order():
quotation = _get_cart_quotation()
quotation.company = frappe.db.get_value("Shopping Cart Settings", None, "company")
for fieldname in ["customer_address", "shipping_address_name"]:
if not quotation.get(fieldname):
throw(_("{0} is required").format(quotation.meta.get_label(fieldname)))
quotation.ignore_permissions = True
quotation.submit()
if quotation.lead:
# company used to create customer accounts
frappe.defaults.set_user_default("company", quotation.company)
from erpnext.selling.doctype.quotation.quotation import _make_sales_order
sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True))
for item in sales_order.get("sales_order_details"):
item.reserved_warehouse = frappe.db.get_value("Item", item.item_code, "website_warehouse") or None
sales_order.ignore_permissions = True
sales_order.insert()
sales_order.submit()
frappe.local.cookie_manager.delete_cookie("cart_count")
return sales_order.name
@frappe.whitelist()
def update_cart(item_code, qty, with_doc):
quotation = _get_cart_quotation()
qty = flt(qty)
if qty == 0:
quotation.set("quotation_details", quotation.get("quotation_details", {"item_code": ["!=", item_code]}))
if not quotation.get("quotation_details") and \
not quotation.get("__islocal"):
quotation.__delete = True
else:
quotation_items = quotation.get("quotation_details", {"item_code": item_code})
if not quotation_items:
quotation.append("quotation_details", {
"doctype": "Quotation Item",
"item_code": item_code,
"qty": qty
})
else:
quotation_items[0].qty = qty
apply_cart_settings(quotation=quotation)
if hasattr(quotation, "__delete"):
frappe.delete_doc("Quotation", quotation.name, ignore_permissions=True)
quotation = _get_cart_quotation()
else:
quotation.ignore_permissions = True
quotation.save()
set_cart_count(quotation)
if with_doc:
return get_cart_quotation(quotation)
else:
return quotation.name
@frappe.whitelist()
def update_cart_address(address_fieldname, address_name):
quotation = _get_cart_quotation()
address_display = get_address_display(frappe.get_doc("Address", address_name).as_dict())
if address_fieldname == "shipping_address_name":
quotation.shipping_address_name = address_name
quotation.shipping_address = address_display
if not quotation.customer_address:
address_fieldname == "customer_address"
if address_fieldname == "customer_address":
quotation.customer_address = address_name
quotation.address_display = address_display
apply_cart_settings(quotation=quotation)
quotation.ignore_permissions = True
quotation.save()
return get_cart_quotation(quotation)
def guess_territory():
territory = None
geoip_country = frappe.session.get("session_country")
if geoip_country:
territory = frappe.db.get_value("Territory", geoip_country)
return territory or \
frappe.db.get_value("Shopping Cart Settings", None, "territory") or \
get_root_of("Territory")
def decorate_quotation_doc(quotation_doc):
doc = frappe._dict(quotation_doc.as_dict())
for d in doc.get("quotation_details", []):
d.update(frappe.db.get_value("Item", d["item_code"],
["website_image", "description", "page_name"], as_dict=True))
d["formatted_rate"] = fmt_money(d.get("rate"), currency=doc.currency)
d["formatted_amount"] = fmt_money(d.get("amount"), currency=doc.currency)
for d in doc.get("other_charges", []):
d["formatted_tax_amount"] = fmt_money(flt(d.get("tax_amount")) / doc.conversion_rate,
currency=doc.currency)
doc.formatted_grand_total_export = fmt_money(doc.grand_total_export,
currency=doc.currency)
return doc
def _get_cart_quotation(party=None):
if not party:
party = get_lead_or_customer()
quotation = frappe.db.get_value("Quotation",
{party.doctype.lower(): party.name, "order_type": "Shopping Cart", "docstatus": 0})
if quotation:
qdoc = frappe.get_doc("Quotation", quotation)
else:
qdoc = frappe.get_doc({
"doctype": "Quotation",
"naming_series": frappe.defaults.get_user_default("shopping_cart_quotation_series") or "QTN-CART-",
"quotation_to": party.doctype,
"company": frappe.db.get_value("Shopping Cart Settings", None, "company"),
"order_type": "Shopping Cart",
"status": "Draft",
"docstatus": 0,
"__islocal": 1,
(party.doctype.lower()): party.name
})
if party.doctype == "Customer":
qdoc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user,
"customer": party.name})
qdoc.ignore_permissions = True
qdoc.run_method("set_missing_values")
apply_cart_settings(party, qdoc)
return qdoc
def update_party(fullname, company_name=None, mobile_no=None, phone=None):
party = get_lead_or_customer()
if party.doctype == "Lead":
party.company_name = company_name
party.lead_name = fullname
party.mobile_no = mobile_no
party.phone = phone
else:
party.customer_name = company_name or fullname
party.customer_type == "Company" if company_name else "Individual"
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user,
"customer": party.name})
contact = frappe.get_doc("Contact", contact_name)
contact.first_name = fullname
contact.last_name = None
contact.customer_name = party.customer_name
contact.mobile_no = mobile_no
contact.phone = phone
contact.ignore_permissions = True
contact.save()
party_doc = frappe.get_doc(party.as_dict())
party_doc.ignore_permissions = True
party_doc.save()
qdoc = _get_cart_quotation(party)
if not qdoc.get("__islocal"):
qdoc.customer_name = company_name or fullname
qdoc.run_method("set_missing_lead_customer_details")
qdoc.ignore_permissions = True
qdoc.save()
def apply_cart_settings(party=None, quotation=None):
if not party:
party = get_lead_or_customer()
if not quotation:
quotation = _get_cart_quotation(party)
cart_settings = frappe.get_doc("Shopping Cart Settings")
billing_territory = get_address_territory(quotation.customer_address) or \
party.territory or get_root_of("Territory")
set_price_list_and_rate(quotation, cart_settings, billing_territory)
quotation.run_method("calculate_taxes_and_totals")
set_taxes(quotation, cart_settings, billing_territory)
_apply_shipping_rule(party, quotation, cart_settings)
def set_price_list_and_rate(quotation, cart_settings, billing_territory):
"""set price list based on billing territory"""
quotation.selling_price_list = cart_settings.get_price_list(billing_territory)
# reset values
quotation.price_list_currency = quotation.currency = \
quotation.plc_conversion_rate = quotation.conversion_rate = None
for item in quotation.get("quotation_details"):
item.price_list_rate = item.discount_percentage = item.rate = item.amount = None
# refetch values
quotation.run_method("set_price_list_and_item_details")
# set it in cookies for using in product page
frappe.local.cookie_manager.set_cookie("selling_price_list", quotation.selling_price_list)
def set_taxes(quotation, cart_settings, billing_territory):
"""set taxes based on billing territory"""
quotation.taxes_and_charges = cart_settings.get_tax_master(billing_territory)
# clear table
quotation.set("other_charges", [])
# append taxes
quotation.append_taxes_from_master("other_charges", "taxes_and_charges")
def get_lead_or_customer():
customer = frappe.db.get_value("Contact", {"email_id": frappe.session.user}, "customer")
if customer:
return frappe.get_doc("Customer", customer)
lead = frappe.db.get_value("Lead", {"email_id": frappe.session.user})
if lead:
return frappe.get_doc("Lead", lead)
else:
lead_doc = frappe.get_doc({
"doctype": "Lead",
"email_id": frappe.session.user,
"lead_name": get_fullname(frappe.session.user),
"territory": guess_territory(),
"status": "Open" # TODO: set something better???
})
if frappe.session.user not in ("Guest", "Administrator"):
lead_doc.ignore_permissions = True
lead_doc.insert()
return lead_doc
def get_address_docs(party=None):
if not party:
party = get_lead_or_customer()
address_docs = frappe.db.sql("""select * from `tabAddress`
where `%s`=%s order by name""" % (party.doctype.lower(), "%s"), party.name,
as_dict=True, update={"doctype": "Address"})
for address in address_docs:
address.display = get_address_display(address)
address.display = (address.display).replace("\n", "<br>\n")
return address_docs
@frappe.whitelist()
def apply_shipping_rule(shipping_rule):
quotation = _get_cart_quotation()
quotation.shipping_rule = shipping_rule
apply_cart_settings(quotation=quotation)
quotation.ignore_permissions = True
quotation.save()
return get_cart_quotation(quotation)
def _apply_shipping_rule(party=None, quotation=None, cart_settings=None):
shipping_rules = get_shipping_rules(party, quotation, cart_settings)
if not shipping_rules:
return
elif quotation.shipping_rule not in shipping_rules:
quotation.shipping_rule = shipping_rules[0]
quotation.run_method("apply_shipping_rule")
quotation.run_method("calculate_taxes_and_totals")
def get_applicable_shipping_rules(party=None, quotation=None):
shipping_rules = get_shipping_rules(party, quotation)
if shipping_rules:
rule_label_map = frappe.db.get_values("Shipping Rule", shipping_rules, "label")
# we need this in sorted order as per the position of the rule in the settings page
return [[rule, rule_label_map.get(rule)] for rule in shipping_rules]
def get_shipping_rules(party=None, quotation=None, cart_settings=None):
if not party:
party = get_lead_or_customer()
if not quotation:
quotation = _get_cart_quotation()
if not cart_settings:
cart_settings = frappe.get_doc("Shopping Cart Settings")
# set shipping rule based on shipping territory
shipping_territory = get_address_territory(quotation.shipping_address_name) or \
party.territory
shipping_rules = cart_settings.get_shipping_rules(shipping_territory)
return shipping_rules
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 = frappe.db.get_value("Address", address_name,
["city", "state", "country"])
for value in address_fields:
territory = frappe.db.get_value("Territory", value)
if territory:
break
return territory
import unittest
test_dependencies = ["Item", "Price List", "Contact", "Shopping Cart Settings"]
class TestCart(unittest.TestCase):
def tearDown(self):
return
cart_settings = frappe.get_doc("Shopping Cart Settings")
cart_settings.ignore_permissions = True
cart_settings.enabled = 0
cart_settings.save()
def enable_shopping_cart(self):
return
if not frappe.db.get_value("Shopping Cart Settings", None, "enabled"):
cart_settings = frappe.get_doc("Shopping Cart Settings")
cart_settings.ignore_permissions = True
cart_settings.enabled = 1
cart_settings.save()
def test_get_lead_or_customer(self):
frappe.session.user = "test@example.com"
party1 = get_lead_or_customer()
party2 = get_lead_or_customer()
self.assertEquals(party1.name, party2.name)
self.assertEquals(party1.doctype, "Lead")
frappe.session.user = "test_contact_customer@example.com"
party = get_lead_or_customer()
self.assertEquals(party.name, "_Test Customer")
def test_add_to_cart(self):
self.enable_shopping_cart()
frappe.session.user = "test@example.com"
update_cart("_Test Item", 1)
quotation = _get_cart_quotation()
quotation_items = quotation.get("quotation_details", {"item_code": "_Test Item"})
self.assertTrue(quotation_items)
self.assertEquals(quotation_items[0].qty, 1)
return quotation
def test_update_cart(self):
self.test_add_to_cart()
update_cart("_Test Item", 5)
quotation = _get_cart_quotation()
quotation_items = quotation.get("quotation_details", {"item_code": "_Test Item"})
self.assertTrue(quotation_items)
self.assertEquals(quotation_items[0].qty, 5)
return quotation
def test_remove_from_cart(self):
quotation0 = self.test_add_to_cart()
update_cart("_Test Item", 0)
quotation = _get_cart_quotation()
self.assertEquals(quotation0.name, quotation.name)
quotation_items = quotation.get("quotation_details", {"item_code": "_Test Item"})
self.assertEquals(quotation_items, [])
def test_place_order(self):
quotation = self.test_update_cart()
sales_order_name = place_order()
sales_order = frappe.get_doc("Sales Order", sales_order_name)
self.assertEquals(sales_order.getone({"item_code": "_Test Item"}).prevdoc_docname, quotation.name)

View File

@@ -0,0 +1,23 @@
{
"creation": "2013-06-20 16:00:18.000000",
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"fieldname": "selling_price_list",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Price List",
"options": "Price List",
"permlevel": 0,
"reqd": 1
}
],
"idx": 1,
"istable": 1,
"modified": "2013-12-20 19:30:47.000000",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Price List",
"owner": "Administrator"
}

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ShoppingCartPriceList(Document):
pass

View File

@@ -0,0 +1,10 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
$.extend(cur_frm.cscript, {
onload: function() {
if(cur_frm.doc.__onload && cur_frm.doc.__onload.quotation_series) {
cur_frm.fields_dict.quotation_series.df.options = cur_frm.doc.__onload.quotation_series;
}
}
});

View File

@@ -0,0 +1,115 @@
{
"creation": "2013-06-19 15:57:32",
"description": "Default settings for Shopping Cart",
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enable Shopping Cart",
"permlevel": 0
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"permlevel": 0
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"permlevel": 0,
"reqd": 1
},
{
"description": "<a href=\"#Sales Browser/Territory\">Add / Edit</a>",
"fieldname": "default_territory",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Default Territory",
"options": "Territory",
"permlevel": 0,
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"permlevel": 0
},
{
"description": "<a href=\"#Sales Browser/Customer Group\">Add / Edit</a>",
"fieldname": "default_customer_group",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default Customer Group",
"options": "Customer Group",
"permlevel": 0,
"reqd": 1
},
{
"fieldname": "quotation_series",
"fieldtype": "Select",
"label": "Quotation Series",
"permlevel": 0,
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"permlevel": 0
},
{
"fieldname": "price_lists",
"fieldtype": "Table",
"label": "Shopping Cart Price Lists",
"options": "Shopping Cart Price List",
"permlevel": 0,
"reqd": 0
},
{
"fieldname": "shipping_rules",
"fieldtype": "Table",
"label": "Shopping Cart Shipping Rules",
"options": "Shopping Cart Shipping Rule",
"permlevel": 0,
"reqd": 0
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"permlevel": 0
},
{
"fieldname": "sales_taxes_and_charges_masters",
"fieldtype": "Table",
"label": "Shopping Cart Taxes and Charges Masters",
"options": "Shopping Cart Taxes and Charges Master",
"permlevel": 0,
"reqd": 0
}
],
"icon": "icon-shopping-cart",
"idx": 1,
"issingle": 1,
"modified": "2014-05-26 03:05:53.747800",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"role": "Website Manager",
"write": 1
}
]
}

View File

@@ -0,0 +1,215 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import comma_and
from frappe.model.document import Document
from frappe.utils.nestedset import get_ancestors_of
from erpnext.utilities.doctype.address.address import get_territory_from_address
class ShoppingCartSetupError(frappe.ValidationError): pass
class ShoppingCartSettings(Document):
def onload(self):
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
def validate(self):
if self.enabled:
self.validate_price_lists()
self.validate_tax_masters()
self.validate_exchange_rates_exist()
def on_update(self):
frappe.db.set_default("shopping_cart_enabled", self.get("enabled") or 0)
frappe.db.set_default("shopping_cart_quotation_series", self.get("quotation_series"))
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:
frappe.throw(_("{0} {1} has a common territory {2}").format(_(doctype), comma_and(names), territory), ShoppingCartSetupError)
return territory_name_map
def validate_price_lists(self):
territory_name_map = self.validate_overlapping_territories("price_lists", "selling_price_list")
# validate that a Shopping Cart Price List exists for the default territory as a catch all!
price_list_for_default_territory = self.get_name_from_territory(self.default_territory, "price_lists",
"selling_price_list")
if not price_list_for_default_territory:
msgprint(_("Please specify a Price List which is valid for Territory") +
": " + self.default_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.get(fieldname) for doc in self.get(parentfield)]
if names:
# for condition in territory check
parenttype = frappe.get_meta(self.meta.get_options(parentfield)).get_options(fieldname)
# 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 = frappe.db.sql("""select `territory`, `parent`
from `tabApplicable 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)
if len(territory_name_map[territory]) > 1:
territory_name_map[territory].sort(key=lambda val: names.index(val))
return territory_name_map
def validate_exchange_rates_exist(self):
"""check if exchange rates exist for all Price List currencies (to company's currency)"""
company_currency = frappe.db.get_value("Company", self.company, "default_currency")
if not company_currency:
msgprint(_("Please specify currency in Company") + ": " + self.company,
raise_exception=ShoppingCartSetupError)
price_list_currency_map = frappe.db.get_values("Price List",
[d.selling_price_list for d in self.get("price_lists")],
"currency")
# check if all price lists have a currency
for price_list, currency in price_list_currency_map.items():
if not currency:
frappe.throw(_("Currency is required for Price List {0}").format(price_list))
expected_to_exist = [currency + "-" + company_currency
for currency in price_list_currency_map.values()
if currency != company_currency]
if expected_to_exist:
exists = frappe.db.sql_list("""select name from `tabCurrency Exchange`
where name in (%s)""" % (", ".join(["%s"]*len(expected_to_exist)),),
tuple(expected_to_exist))
missing = list(set(expected_to_exist).difference(exists))
if missing:
msgprint(_("Missing Currency Exchange Rates for {0}").format(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)
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)
break
return name
def get_price_list(self, billing_territory):
price_list = self.get_name_from_territory(billing_territory, "price_lists", "selling_price_list")
return price_list and price_list[0] or None
def get_tax_master(self, billing_territory):
tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
return tax_master and tax_master[0] or None
def get_shipping_rules(self, shipping_territory):
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
def get_territory_ancestry(self, territory):
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]
def validate_cart_settings(doc, method):
frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate")
def get_shopping_cart_settings():
if not getattr(frappe.local, "shopping_cart_settings", None):
frappe.local.shopping_cart_settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
return frappe.local.shopping_cart_settings
def get_default_territory():
return get_shopping_cart_settings().default_territory
def is_shopping_cart_enabled():
if not get_shopping_cart_settings().enabled:
frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError)
def apply_shopping_cart_settings(quotation, method):
"""Called via a validate hook on Quotation"""
from erpnext.shopping_cart import get_party
if quotation.order_type != "Shopping Cart":
return
quotation.billing_territory = (get_territory_from_address(quotation.customer_address)
or get_party(quotation.contact_email).territory or get_default_territory())
quotation.shipping_territory = (get_territory_from_address(quotation.shipping_address_name)
or get_party(quotation.contact_email).territory or get_default_territory())
set_price_list(quotation)
set_taxes_and_charges(quotation)
quotation.calculate_taxes_and_totals()
set_shipping_rule(quotation)
def set_price_list(quotation):
previous_selling_price_list = quotation.selling_price_list
quotation.selling_price_list = get_shopping_cart_settings().get_price_list(quotation.billing_territory)
if not quotation.selling_price_list:
quotation.selling_price_list = get_shopping_cart_settings().get_price_list(get_default_territory())
if previous_selling_price_list != quotation.selling_price_list:
quotation.price_list_currency = quotation.currency = quotation.plc_conversion_rate = quotation.conversion_rate = None
for d in quotation.get("quotation_details"):
d.price_list_rate = d.discount_percentage = d.rate = d.amount = None
quotation.set_price_list_and_item_details()
def set_taxes_and_charges(quotation):
previous_taxes_and_charges = quotation.taxes_and_charges
quotation.taxes_and_charges = get_shopping_cart_settings().get_tax_master(quotation.billing_territory)
if previous_taxes_and_charges != quotation.taxes_and_charges:
quotation.set_other_charges()
def set_shipping_rule(quotation):
shipping_rules = get_shopping_cart_settings().get_shipping_rules(quotation.shipping_territory)
if not shipping_rules:
quotation.remove_shipping_charge()
return
if quotation.shipping_rule not in shipping_rules:
quotation.remove_shipping_charge()
quotation.shipping_rule = shipping_rules[0]
quotation.apply_shipping_rule()

View File

@@ -0,0 +1,79 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ShoppingCartSetupError
class TestShoppingCartSettings(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
frappe.db.sql("""delete from `tabShopping Cart Price List`""")
frappe.db.sql("""delete from `tabShopping Cart Taxes and Charges Master`""")
frappe.db.sql("""delete from `tabShopping Cart Shipping Rule`""")
def get_cart_settings(self):
return frappe.get_doc({"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.append("price_lists", {
"doctype": "Shopping Cart Price List",
"selling_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
controller.validate_overlapping_territories("price_lists", "selling_price_list")
_add_price_list("_Test Price List 2")
controller = cart_settings
self.assertRaises(ShoppingCartSetupError, controller.validate_overlapping_territories,
"price_lists", "selling_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.append("sales_taxes_and_charges_masters", {
"doctype": "Shopping Cart Taxes and Charges Master",
"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
controller.validate_overlapping_territories("sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master")
_add_tax_master("_Test Sales Taxes and Charges Master - Rest of the World")
controller = cart_settings
self.assertRaises(ShoppingCartSetupError, controller.validate_overlapping_territories,
"sales_taxes_and_charges_masters", "sales_taxes_and_charges_master")
def test_exchange_rate_exists(self):
frappe.db.sql("""delete from `tabCurrency Exchange`""")
cart_settings = self.test_price_list_territory_overlap()
controller = cart_settings
self.assertRaises(ShoppingCartSetupError, controller.validate_exchange_rates_exist)
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
currency_exchange_records
frappe.get_doc(currency_exchange_records[0]).insert()
controller.validate_exchange_rates_exist()

View File

@@ -0,0 +1,23 @@
{
"creation": "2013-07-03 13:15:34.000000",
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"fieldname": "shipping_rule",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Shipping Rule",
"options": "Shipping Rule",
"permlevel": 0,
"reqd": 1
}
],
"idx": 1,
"istable": 1,
"modified": "2013-12-20 19:30:47.000000",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Shipping Rule",
"owner": "Administrator"
}

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ShoppingCartShippingRule(Document):
pass

View File

@@ -0,0 +1,23 @@
{
"creation": "2013-06-20 16:57:03.000000",
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"fieldname": "sales_taxes_and_charges_master",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax Master",
"options": "Sales Taxes and Charges Master",
"permlevel": 0,
"reqd": 1
}
],
"idx": 1,
"istable": 1,
"modified": "2013-12-20 19:30:47.000000",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Taxes and Charges Master",
"owner": "Administrator"
}

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ShoppingCartTaxesandChargesMaster(Document):
pass

View File

@@ -0,0 +1,54 @@
# 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 frappe
from frappe.utils import cint, fmt_money, cstr
from erpnext.shopping_cart.cart import _get_cart_quotation
from urllib import unquote
@frappe.whitelist(allow_guest=True)
def get_product_info(item_code):
"""get product price / stock info"""
if not cint(frappe.db.get_default("shopping_cart_enabled")):
return {}
cart_quotation = _get_cart_quotation()
price_list = cstr(unquote(frappe.local.request.cookies.get("selling_price_list")))
warehouse = frappe.db.get_value("Item", item_code, "website_warehouse")
if warehouse:
in_stock = frappe.db.sql("""select actual_qty from tabBin where
item_code=%s and warehouse=%s""", (item_code, warehouse))
if in_stock:
in_stock = in_stock[0][0] > 0 and 1 or 0
else:
in_stock = -1
price = price_list and frappe.db.sql("""select price_list_rate, currency from
`tabItem Price` where item_code=%s and price_list=%s""",
(item_code, price_list), as_dict=1) or []
price = price and price[0] or None
qty = 0
if price:
price["formatted_price"] = fmt_money(price["price_list_rate"], currency=price["currency"])
price["currency"] = not cint(frappe.db.get_default("hide_currency_symbol")) \
and (frappe.db.get_value("Currency", price.currency, "symbol") or price.currency) \
or ""
if frappe.session.user != "Guest":
item = cart_quotation.get({"item_code": item_code})
if item:
qty = item[0].qty
return {
"price": price,
"stock": in_stock,
"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
"qty": qty
}

View File

@@ -0,0 +1,230 @@
# 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
from erpnext.shopping_cart import get_quotation, set_item_in_cart
class TestShoppingCart(unittest.TestCase):
"""
Note:
Shopping Cart == Quotation
"""
def setUp(self):
frappe.set_user("Administrator")
self.enable_shopping_cart()
def tearDown(self):
frappe.set_user("Administrator")
self.disable_shopping_cart()
def test_get_cart_new_user(self):
self.login_as_new_user()
# test if lead is created and quotation with new lead is fetched
quotation = get_quotation()
self.assertEquals(quotation.quotation_to, "Lead")
self.assertEquals(frappe.db.get_value("Lead", quotation.lead, "email_id"), "test_cart_user@example.com")
self.assertEquals(quotation.customer, None)
self.assertEquals(quotation.contact_email, frappe.session.user)
return quotation
def test_get_cart_lead(self):
self.login_as_lead()
# test if quotation with lead is fetched
quotation = get_quotation()
self.assertEquals(quotation.quotation_to, "Lead")
self.assertEquals(quotation.lead, frappe.db.get_value("Lead", {"email_id": "test_cart_lead@example.com"}))
self.assertEquals(quotation.customer, None)
self.assertEquals(quotation.contact_email, frappe.session.user)
return quotation
def test_get_cart_customer(self):
self.login_as_customer()
# test if quotation with customer is fetched
quotation = get_quotation()
self.assertEquals(quotation.quotation_to, "Customer")
self.assertEquals(quotation.customer, "_Test Customer")
self.assertEquals(quotation.lead, None)
self.assertEquals(quotation.contact_email, frappe.session.user)
return quotation
def test_add_to_cart(self):
self.login_as_lead()
# add first item
set_item_in_cart("_Test Item", 1)
quotation = self.test_get_cart_lead()
self.assertEquals(quotation.get("quotation_details")[0].item_code, "_Test Item")
self.assertEquals(quotation.get("quotation_details")[0].qty, 1)
self.assertEquals(quotation.get("quotation_details")[0].amount, 10)
# add second item
set_item_in_cart("_Test Item 2", 1)
quotation = self.test_get_cart_lead()
self.assertEquals(quotation.get("quotation_details")[1].item_code, "_Test Item 2")
self.assertEquals(quotation.get("quotation_details")[1].qty, 1)
self.assertEquals(quotation.get("quotation_details")[1].amount, 20)
self.assertEquals(len(quotation.get("quotation_details")), 2)
def test_update_cart(self):
# first, add to cart
self.test_add_to_cart()
# update first item
set_item_in_cart("_Test Item", 5)
quotation = self.test_get_cart_lead()
self.assertEquals(quotation.get("quotation_details")[0].item_code, "_Test Item")
self.assertEquals(quotation.get("quotation_details")[0].qty, 5)
self.assertEquals(quotation.get("quotation_details")[0].amount, 50)
self.assertEquals(quotation.net_total, 70)
self.assertEquals(len(quotation.get("quotation_details")), 2)
def test_remove_from_cart(self):
# first, add to cart
self.test_add_to_cart()
# remove first item
set_item_in_cart("_Test Item", 0)
quotation = self.test_get_cart_lead()
self.assertEquals(quotation.get("quotation_details")[0].item_code, "_Test Item 2")
self.assertEquals(quotation.get("quotation_details")[0].qty, 1)
self.assertEquals(quotation.get("quotation_details")[0].amount, 20)
self.assertEquals(quotation.net_total, 20)
self.assertEquals(len(quotation.get("quotation_details")), 1)
# remove second item
set_item_in_cart("_Test Item 2", 0)
quotation = self.test_get_cart_lead()
self.assertEquals(quotation.net_total, 0)
self.assertEquals(len(quotation.get("quotation_details")), 0)
def test_set_billing_address(self):
return
# first, add to cart
self.test_add_to_cart()
quotation = self.test_get_cart_lead()
default_address = frappe.get_doc("Address", {"lead": quotation.lead, "is_primary_address": 1})
self.assertEquals("customer_address", default_address.name)
def test_set_shipping_address(self):
# first, add to cart
self.test_add_to_cart()
def test_shipping_rule(self):
self.test_set_shipping_address()
# check if shipping rule changed
pass
def test_price_list(self):
self.test_set_billing_address()
# check if price changed
pass
def test_place_order(self):
pass
# helper functions
def enable_shopping_cart(self):
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
if settings.default_territory == "_Test Territory Rest Of The World":
settings.enabled = 1
else:
settings.update({
"enabled": 1,
"company": "_Test Company",
"default_territory": "_Test Territory Rest Of The World",
"default_customer_group": "_Test Customer Group",
"quotation_series": "_T-Quotation-"
})
settings.set("price_lists", [
# price lists
{"doctype": "Shopping Cart Price List", "parentfield": "price_lists",
"selling_price_list": "_Test Price List India"},
{"doctype": "Shopping Cart Price List", "parentfield": "price_lists",
"selling_price_list": "_Test Price List Rest of the World"}
])
settings.set("sales_taxes_and_charges_masters", [
# tax masters
{"doctype": "Shopping Cart Taxes and Charges Master", "parentfield": "sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master": "_Test India Tax Master"},
{"doctype": "Shopping Cart Taxes and Charges Master", "parentfield": "sales_taxes_and_charges_masters",
"sales_taxes_and_charges_master": "_Test Sales Taxes and Charges Master - Rest of the World"},
])
settings.set("shipping_rules", {"doctype": "Shopping Cart Shipping Rule", "parentfield": "shipping_rules",
"shipping_rule": "_Test Shipping Rule - India"})
settings.save()
frappe.local.shopping_cart_settings = None
def disable_shopping_cart(self):
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
settings.enabled = 0
settings.save()
frappe.local.shopping_cart_settings = None
def login_as_new_user(self):
frappe.set_user("test_cart_user@example.com")
def login_as_lead(self):
self.create_lead()
frappe.set_user("test_cart_lead@example.com")
def login_as_customer(self):
frappe.set_user("test_contact_customer@example.com")
def create_lead(self):
if frappe.db.get_value("Lead", {"email_id": "test_cart_lead@example.com"}):
return
lead = frappe.get_doc({
"doctype": "Lead",
"email_id": "test_cart_lead@example.com",
"lead_name": "_Test Website Lead",
"status": "Open",
"territory": "_Test Territory Rest Of The World"
})
lead.insert(ignore_permissions=True)
frappe.get_doc({
"doctype": "Address",
"address_line1": "_Test Address Line 1",
"address_title": "_Test Cart Lead Address",
"address_type": "Office",
"city": "_Test City",
"country": "United States",
"lead": lead.name,
"lead_name": "_Test Website Lead",
"is_primary_address": 1,
"phone": "+91 0000000000"
}).insert(ignore_permissions=True)
frappe.get_doc({
"doctype": "Address",
"address_line1": "_Test Address Line 1",
"address_title": "_Test Cart Lead Address",
"address_type": "Home",
"city": "_Test City",
"country": "India",
"lead": lead.name,
"lead_name": "_Test Website Lead",
"phone": "+91 0000000000"
}).insert(ignore_permissions=True)
test_dependencies = ["Sales Taxes and Charges Master", "Price List", "Item Price", "Shipping Rule", "Currency Exchange",
"Customer Group", "Lead", "Customer", "Contact", "Address", "Item"]

View File

@@ -0,0 +1,47 @@
# 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 frappe
import frappe.defaults
from frappe.utils import cint
def show_cart_count():
if (frappe.db.get_default("shopping_cart_enabled") and
frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User"):
return True
return False
def set_cart_count(login_manager):
if show_cart_count():
from .shopping_cart.cart import set_cart_count
set_cart_count()
def clear_cart_count(login_manager):
if show_cart_count():
frappe.local.cookie_manager.delete_cookie("cart_count")
def update_website_context(context):
post_login = []
cart_enabled = cint(frappe.db.get_default("shopping_cart_enabled"))
context["shopping_cart_enabled"] = cart_enabled
if cart_enabled:
post_login += [
{"label": "Cart", "url": "cart", "icon": "icon-shopping-cart", "class": "cart-count"},
{"class": "divider"}
]
post_login += [
{"label": "User", "url": "user", "icon": "icon-user"},
{"label": "Addresses", "url": "addresses", "icon": "icon-map-marker"},
{"label": "My Orders", "url": "orders", "icon": "icon-list"},
{"label": "My Tickets", "url": "tickets", "icon": "icon-tags"},
{"label": "Invoices", "url": "invoices", "icon": "icon-file-text"},
{"label": "Shipments", "url": "shipments", "icon": "icon-truck"},
{"class": "divider"}
]
context["post_login"] = post_login + context.get("post_login", [])