61c781325d
In case of order being placed with stock quantity more than available, message showing stock available wasn't showing due to tuple unpacking not happening correctly.
486 lines
14 KiB
Python
486 lines
14 KiB
Python
# Copyright (c) 2015, Frappe 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 cint, flt, get_fullname, cstr
|
|
from frappe.contacts.doctype.address.address import get_address_display
|
|
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings
|
|
from frappe.utils.nestedset import get_root_of
|
|
from erpnext.accounts.utils import get_account_name
|
|
from erpnext.utilities.product import get_qty_in_stock
|
|
|
|
|
|
class WebsitePriceListMissingError(frappe.ValidationError):
|
|
pass
|
|
|
|
def set_cart_count(quotation=None):
|
|
if cint(frappe.db.get_singles_value("Shopping Cart Settings", "enabled")):
|
|
if not quotation:
|
|
quotation = _get_cart_quotation()
|
|
cart_count = cstr(len(quotation.get("items")))
|
|
|
|
if hasattr(frappe.local, "cookie_manager"):
|
|
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
|
|
|
|
@frappe.whitelist()
|
|
def get_cart_quotation(doc=None):
|
|
party = get_party()
|
|
|
|
if not doc:
|
|
quotation = _get_cart_quotation(party)
|
|
doc = quotation
|
|
set_cart_count(quotation)
|
|
|
|
addresses = get_address_docs(party=party)
|
|
|
|
if not doc.customer_address and addresses:
|
|
update_cart_address("customer_address", addresses[0].name)
|
|
|
|
return {
|
|
"doc": decorate_quotation_doc(doc),
|
|
"shipping_addresses": [{"name": address.name, "display": address.display}
|
|
for address in addresses],
|
|
"billing_addresses": [{"name": address.name, "display": address.display}
|
|
for address in addresses],
|
|
"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")
|
|
if not quotation.get("customer_address"):
|
|
throw(_("{0} is required").format(_(quotation.meta.get_label("customer_address"))))
|
|
|
|
quotation.flags.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("items"):
|
|
item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
|
|
item.item_code, ["website_warehouse", "is_stock_item"])
|
|
|
|
if is_stock_item:
|
|
item_stock = get_qty_in_stock(item.item_code, "website_warehouse")
|
|
if item.qty > item_stock.stock_qty[0][0]:
|
|
throw(_("Only {0} in stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
|
|
|
|
sales_order.flags.ignore_permissions = True
|
|
sales_order.insert()
|
|
sales_order.submit()
|
|
|
|
if hasattr(frappe.local, "cookie_manager"):
|
|
frappe.local.cookie_manager.delete_cookie("cart_count")
|
|
|
|
return sales_order.name
|
|
|
|
@frappe.whitelist()
|
|
def update_cart(item_code, qty, with_items=False):
|
|
quotation = _get_cart_quotation()
|
|
|
|
empty_card = False
|
|
qty = flt(qty)
|
|
if qty == 0:
|
|
quotation_items = quotation.get("items", {"item_code": ["!=", item_code]})
|
|
if quotation_items:
|
|
quotation.set("items", quotation_items)
|
|
else:
|
|
empty_card = True
|
|
|
|
else:
|
|
quotation_items = quotation.get("items", {"item_code": item_code})
|
|
if not quotation_items:
|
|
quotation.append("items", {
|
|
"doctype": "Quotation Item",
|
|
"item_code": item_code,
|
|
"qty": qty
|
|
})
|
|
else:
|
|
quotation_items[0].qty = qty
|
|
|
|
apply_cart_settings(quotation=quotation)
|
|
|
|
quotation.flags.ignore_permissions = True
|
|
quotation.payment_schedule = []
|
|
if not empty_card:
|
|
quotation.save()
|
|
else:
|
|
quotation.delete()
|
|
quotation = None
|
|
|
|
set_cart_count(quotation)
|
|
|
|
context = get_cart_quotation(quotation)
|
|
|
|
if cint(with_items):
|
|
return {
|
|
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
|
context),
|
|
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
|
|
context),
|
|
}
|
|
else:
|
|
return {
|
|
'name': quotation.name,
|
|
'shopping_cart_menu': get_shopping_cart_menu(context)
|
|
}
|
|
|
|
@frappe.whitelist()
|
|
def get_shopping_cart_menu(context=None):
|
|
if not context:
|
|
context = get_cart_quotation()
|
|
|
|
return frappe.render_template('templates/includes/cart/cart_dropdown.html', context)
|
|
|
|
@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.flags.ignore_permissions = True
|
|
quotation.save()
|
|
|
|
context = get_cart_quotation(quotation)
|
|
return {
|
|
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
|
|
context),
|
|
}
|
|
|
|
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(doc):
|
|
for d in doc.get("items", []):
|
|
d.update(frappe.db.get_value("Item", d.item_code,
|
|
["thumbnail", "website_image", "description", "route"], as_dict=True))
|
|
|
|
return doc
|
|
|
|
|
|
def _get_cart_quotation(party=None):
|
|
'''Return the open Quotation of type "Shopping Cart" or make a new one'''
|
|
if not party:
|
|
party = get_party()
|
|
|
|
quotation = frappe.get_all("Quotation", fields=["name"], filters=
|
|
{party.doctype.lower(): party.name, "order_type": "Shopping Cart", "docstatus": 0},
|
|
order_by="modified desc", limit_page_length=1)
|
|
|
|
if quotation:
|
|
qdoc = frappe.get_doc("Quotation", quotation[0].name)
|
|
else:
|
|
qdoc = frappe.get_doc({
|
|
"doctype": "Quotation",
|
|
"naming_series": get_shopping_cart_settings().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
|
|
})
|
|
|
|
qdoc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
|
|
qdoc.contact_email = frappe.session.user
|
|
|
|
qdoc.flags.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_party()
|
|
|
|
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})
|
|
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.flags.ignore_permissions = True
|
|
contact.save()
|
|
|
|
party_doc = frappe.get_doc(party.as_dict())
|
|
party_doc.flags.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.flags.ignore_permissions = True
|
|
qdoc.save()
|
|
|
|
def apply_cart_settings(party=None, quotation=None):
|
|
if not party:
|
|
party = get_party()
|
|
if not quotation:
|
|
quotation = _get_cart_quotation(party)
|
|
|
|
cart_settings = frappe.get_doc("Shopping Cart Settings")
|
|
|
|
set_price_list_and_rate(quotation, cart_settings)
|
|
|
|
quotation.run_method("calculate_taxes_and_totals")
|
|
|
|
set_taxes(quotation, cart_settings)
|
|
|
|
_apply_shipping_rule(party, quotation, cart_settings)
|
|
|
|
def set_price_list_and_rate(quotation, cart_settings):
|
|
"""set price list based on billing territory"""
|
|
|
|
_set_price_list(quotation, cart_settings)
|
|
|
|
# reset values
|
|
quotation.price_list_currency = quotation.currency = \
|
|
quotation.plc_conversion_rate = quotation.conversion_rate = None
|
|
for item in quotation.get("items"):
|
|
item.price_list_rate = item.discount_percentage = item.rate = item.amount = None
|
|
|
|
# refetch values
|
|
quotation.run_method("set_price_list_and_item_details")
|
|
|
|
if hasattr(frappe.local, "cookie_manager"):
|
|
# set it in cookies for using in product page
|
|
frappe.local.cookie_manager.set_cookie("selling_price_list", quotation.selling_price_list)
|
|
|
|
def _set_price_list(quotation, cart_settings):
|
|
"""Set price list based on customer or shopping cart default"""
|
|
if quotation.selling_price_list:
|
|
return
|
|
|
|
# check if customer price list exists
|
|
selling_price_list = None
|
|
if quotation.customer:
|
|
from erpnext.accounts.party import get_default_price_list
|
|
selling_price_list = get_default_price_list(frappe.get_doc("Customer", quotation.customer))
|
|
|
|
# else check for territory based price list
|
|
if not selling_price_list:
|
|
selling_price_list = cart_settings.price_list
|
|
|
|
quotation.selling_price_list = selling_price_list
|
|
|
|
def set_taxes(quotation, cart_settings):
|
|
"""set taxes based on billing territory"""
|
|
from erpnext.accounts.party import set_taxes
|
|
|
|
customer_group = frappe.db.get_value("Customer", quotation.customer, "customer_group")
|
|
|
|
quotation.taxes_and_charges = set_taxes(quotation.customer, "Customer", \
|
|
quotation.transaction_date, quotation.company, customer_group, None, \
|
|
quotation.customer_address, quotation.shipping_address_name, 1)
|
|
#
|
|
# # clear table
|
|
quotation.set("taxes", [])
|
|
#
|
|
# # append taxes
|
|
quotation.append_taxes_from_master()
|
|
|
|
def get_party(user=None):
|
|
if not user:
|
|
user = frappe.session.user
|
|
|
|
contact_name = frappe.db.get_value("Contact", {"email_id": user})
|
|
party = None
|
|
|
|
if contact_name:
|
|
contact = frappe.get_doc('Contact', contact_name)
|
|
if contact.links:
|
|
party_doctype = contact.links[0].link_doctype
|
|
party = contact.links[0].link_name
|
|
|
|
cart_settings = frappe.get_doc("Shopping Cart Settings")
|
|
|
|
debtors_account = ''
|
|
|
|
if cart_settings.enable_checkout:
|
|
debtors_account = get_debtors_account(cart_settings)
|
|
|
|
if party:
|
|
return frappe.get_doc(party_doctype, party)
|
|
|
|
else:
|
|
if not cart_settings.enabled:
|
|
frappe.local.flags.redirect_location = "/contact"
|
|
raise frappe.Redirect
|
|
customer = frappe.new_doc("Customer")
|
|
fullname = get_fullname(user)
|
|
customer.update({
|
|
"customer_name": fullname,
|
|
"customer_type": "Individual",
|
|
"customer_group": get_shopping_cart_settings().default_customer_group,
|
|
"territory": get_root_of("Territory")
|
|
})
|
|
|
|
if debtors_account:
|
|
customer.update({
|
|
"accounts": [{
|
|
"company": cart_settings.company,
|
|
"account": debtors_account
|
|
}]
|
|
})
|
|
|
|
customer.flags.ignore_mandatory = True
|
|
customer.insert(ignore_permissions=True)
|
|
|
|
contact = frappe.new_doc("Contact")
|
|
contact.update({
|
|
"first_name": fullname,
|
|
"email_id": user
|
|
})
|
|
contact.append('links', dict(link_doctype='Customer', link_name=customer.name))
|
|
contact.flags.ignore_mandatory = True
|
|
contact.insert(ignore_permissions=True)
|
|
|
|
return customer
|
|
|
|
def get_debtors_account(cart_settings):
|
|
payment_gateway_account_currency = \
|
|
frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency
|
|
|
|
account_name = _("Debtors ({0})".format(payment_gateway_account_currency))
|
|
|
|
debtors_account_name = get_account_name("Receivable", "Asset", is_group=0,\
|
|
account_currency=payment_gateway_account_currency, company=cart_settings.company)
|
|
|
|
if not debtors_account_name:
|
|
debtors_account = frappe.get_doc({
|
|
"doctype": "Account",
|
|
"account_type": "Receivable",
|
|
"root_type": "Asset",
|
|
"is_group": 0,
|
|
"parent_account": get_account_name(root_type="Asset", is_group=1, company=cart_settings.company),
|
|
"account_name": account_name,
|
|
"currency": payment_gateway_account_currency
|
|
}).insert(ignore_permissions=True)
|
|
|
|
return debtors_account.name
|
|
|
|
else:
|
|
return debtors_account_name
|
|
|
|
|
|
def get_address_docs(doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20,
|
|
party=None):
|
|
if not party:
|
|
party = get_party()
|
|
|
|
if not party:
|
|
return []
|
|
|
|
address_names = frappe.db.get_all('Dynamic Link', fields=('parent'),
|
|
filters=dict(parenttype='Address', link_doctype=party.doctype, link_name=party.name))
|
|
|
|
out = []
|
|
|
|
for a in address_names:
|
|
address = frappe.get_doc('Address', a.parent)
|
|
address.display = get_address_display(address.as_dict())
|
|
out.append(address)
|
|
|
|
return out
|
|
|
|
@frappe.whitelist()
|
|
def apply_shipping_rule(shipping_rule):
|
|
quotation = _get_cart_quotation()
|
|
|
|
quotation.shipping_rule = shipping_rule
|
|
|
|
apply_cart_settings(quotation=quotation)
|
|
|
|
quotation.flags.ignore_permissions = True
|
|
quotation.save()
|
|
|
|
return get_cart_quotation(quotation)
|
|
|
|
def _apply_shipping_rule(party=None, quotation=None, cart_settings=None):
|
|
if not quotation.shipping_rule:
|
|
shipping_rules = get_shipping_rules(quotation, cart_settings)
|
|
|
|
if not shipping_rules:
|
|
return
|
|
|
|
elif quotation.shipping_rule not in shipping_rules:
|
|
quotation.shipping_rule = shipping_rules[0]
|
|
|
|
if quotation.shipping_rule:
|
|
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(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(quotation=None, cart_settings=None):
|
|
if not quotation:
|
|
quotation = _get_cart_quotation()
|
|
|
|
shipping_rules = []
|
|
if quotation.shipping_address_name:
|
|
country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
|
|
if country:
|
|
shipping_rules = frappe.db.sql_list("""select distinct sr.name
|
|
from `tabShipping Rule Country` src, `tabShipping Rule` sr
|
|
where src.country = %s and
|
|
sr.disabled != 1 and sr.name = src.parent""", country)
|
|
|
|
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
|
|
|
|
def show_terms(doc):
|
|
return doc.tc_name
|