- Something went wrong. Please refresh or contact us.
-
- `;
- let no_results_section = `
-
-
-
-
-
${ __('No products found') }
-
- `;
-
- this.products_section.append(error ? error_section : no_results_section);
- }
-
- render_item_sub_categories(categories) {
- if (categories && categories.length) {
- let sub_group_html = `
-
`;
-
- $("#product-listing").prepend(sub_group_html);
- }
- }
-
- get_query_string(object) {
- const url = new URLSearchParams();
- for (let key in object) {
- const value = object[key];
- if (value) {
- url.append(key, value);
- }
- }
- return url.toString();
- }
-
- if_key_exists(obj) {
- let exists = false;
- for (let key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key]) {
- exists = true;
- break;
- }
- }
- return exists ? obj : undefined;
- }
-};
\ No newline at end of file
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py
deleted file mode 100644
index 87ca9bd83d..0000000000
--- a/erpnext/e_commerce/redisearch_utils.py
+++ /dev/null
@@ -1,255 +0,0 @@
-# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import json
-
-import frappe
-from frappe import _
-from frappe.utils.redis_wrapper import RedisWrapper
-from redis import ResponseError
-from redis.commands.search.field import TagField, TextField
-from redis.commands.search.indexDefinition import IndexDefinition
-from redis.commands.search.suggestion import Suggestion
-
-WEBSITE_ITEM_INDEX = "website_items_index"
-WEBSITE_ITEM_KEY_PREFIX = "website_item:"
-WEBSITE_ITEM_NAME_AUTOCOMPLETE = "website_items_name_dict"
-WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = "website_items_category_dict"
-
-
-def get_indexable_web_fields():
- "Return valid fields from Website Item that can be searched for."
- web_item_meta = frappe.get_meta("Website Item", cached=True)
- valid_fields = filter(
- lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"),
- web_item_meta.fields,
- )
-
- return [df.fieldname for df in valid_fields]
-
-
-def is_redisearch_enabled():
- "Return True only if redisearch is loaded and enabled."
- is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled")
- return is_search_module_loaded() and is_redisearch_enabled
-
-
-def is_search_module_loaded():
- try:
- cache = frappe.cache()
- for module in cache.module_list():
- if module.get(b"name") == b"search":
- return True
- except Exception:
- return False # handling older redis versions
-
-
-def if_redisearch_enabled(function):
- "Decorator to check if Redisearch is enabled."
-
- def wrapper(*args, **kwargs):
- if is_redisearch_enabled():
- func = function(*args, **kwargs)
- return func
- return
-
- return wrapper
-
-
-def make_key(key):
- return frappe.cache().make_key(key)
-
-
-@if_redisearch_enabled
-def create_website_items_index():
- "Creates Index Definition."
-
- redis = frappe.cache()
- index = redis.ft(WEBSITE_ITEM_INDEX)
-
- try:
- index.dropindex() # drop if already exists
- except ResponseError:
- # will most likely raise a ResponseError if index does not exist
- # ignore and create index
- pass
- except Exception:
- raise_redisearch_error()
-
- idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
-
- # Index fields mentioned in e-commerce settings
- idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
- idx_fields = idx_fields.split(",") if idx_fields else []
-
- if "web_item_name" in idx_fields:
- idx_fields.remove("web_item_name")
-
- idx_fields = [to_search_field(f) for f in idx_fields]
-
- # TODO: sortable?
- index.create_index(
- [TextField("web_item_name", sortable=True)] + idx_fields,
- definition=idx_def,
- )
-
- reindex_all_web_items()
- define_autocomplete_dictionary()
-
-
-def to_search_field(field):
- if field == "tags":
- return TagField("tags", separator=",")
-
- return TextField(field)
-
-
-@if_redisearch_enabled
-def insert_item_to_index(website_item_doc):
- # Insert item to index
- key = get_cache_key(website_item_doc.name)
- cache = frappe.cache()
- web_item = create_web_item_map(website_item_doc)
-
- for field, value in web_item.items():
- super(RedisWrapper, cache).hset(make_key(key), field, value)
-
- insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
-
-
-@if_redisearch_enabled
-def insert_to_name_ac(web_name, doc_name):
- ac = frappe.cache().ft()
- ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(web_name, payload=doc_name))
-
-
-def create_web_item_map(website_item_doc):
- fields_to_index = get_fields_indexed()
- web_item = {}
-
- for field in fields_to_index:
- web_item[field] = website_item_doc.get(field) or ""
-
- return web_item
-
-
-@if_redisearch_enabled
-def update_index_for_item(website_item_doc):
- # Reinsert to Cache
- insert_item_to_index(website_item_doc)
- define_autocomplete_dictionary()
-
-
-@if_redisearch_enabled
-def delete_item_from_index(website_item_doc):
- cache = frappe.cache()
- key = get_cache_key(website_item_doc.name)
-
- try:
- cache.delete(key)
- except Exception:
- raise_redisearch_error()
-
- delete_from_ac_dict(website_item_doc)
- return True
-
-
-@if_redisearch_enabled
-def delete_from_ac_dict(website_item_doc):
- """Removes this items's name from autocomplete dictionary"""
- ac = frappe.cache().ft()
- ac.sugdel(website_item_doc.web_item_name)
-
-
-@if_redisearch_enabled
-def define_autocomplete_dictionary():
- """
- Defines/Redefines an autocomplete search dictionary for Website Item Name.
- Also creats autocomplete dictionary for Published Item Groups.
- """
-
- cache = frappe.cache()
-
- # Delete both autocomplete dicts
- try:
- cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
- cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
- except Exception:
- raise_redisearch_error()
-
- create_items_autocomplete_dict()
- create_item_groups_autocomplete_dict()
-
-
-@if_redisearch_enabled
-def create_items_autocomplete_dict():
- "Add items as suggestions in Autocompleter."
-
- ac = frappe.cache().ft()
- items = frappe.get_all(
- "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
- )
- for item in items:
- ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(item.web_item_name))
-
-
-@if_redisearch_enabled
-def create_item_groups_autocomplete_dict():
- "Add item groups with weightage as suggestions in Autocompleter."
-
- published_item_groups = frappe.get_all(
- "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
- )
- if not published_item_groups:
- return
-
- ac = frappe.cache().ft()
-
- for item_group in published_item_groups:
- payload = json.dumps({"name": item_group.name, "route": item_group.route})
- ac.sugadd(
- WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
- Suggestion(
- string=item_group.name,
- score=frappe.utils.flt(item_group.weightage) or 1.0,
- payload=payload, # additional info that can be retrieved later
- ),
- )
-
-
-@if_redisearch_enabled
-def reindex_all_web_items():
- items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
-
- cache = frappe.cache()
- for item in items:
- web_item = create_web_item_map(item)
- key = make_key(get_cache_key(item.name))
-
- for field, value in web_item.items():
- super(RedisWrapper, cache).hset(key, field, value)
-
-
-def get_cache_key(name):
- name = frappe.scrub(name)
- return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
-
-
-def get_fields_indexed():
- fields_to_index = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
- fields_to_index = fields_to_index.split(",") if fields_to_index else []
-
- mandatory_fields = ["name", "web_item_name", "route", "thumbnail", "ranking"]
- fields_to_index = fields_to_index + mandatory_fields
-
- return fields_to_index
-
-
-def raise_redisearch_error():
- "Create an Error Log and raise error."
- log = frappe.log_error("Redisearch Error")
- log_link = frappe.utils.get_link_to_form("Error Log", log.name)
-
- frappe.throw(
- msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error")
- )
diff --git a/erpnext/e_commerce/shopping_cart/__init__.py b/erpnext/e_commerce/shopping_cart/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
deleted file mode 100644
index 7c7e169c52..0000000000
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ /dev/null
@@ -1,721 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-import frappe.defaults
-from frappe import _, throw
-from frappe.contacts.doctype.address.address import get_address_display
-from frappe.contacts.doctype.contact.contact import get_contact_name
-from frappe.utils import cint, cstr, flt, get_fullname
-from frappe.utils.nestedset import get_root_of
-
-from erpnext.accounts.utils import get_account_name
-from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
- get_shopping_cart_settings,
-)
-from erpnext.utilities.product import get_web_item_qty_in_stock
-
-
-class WebsitePriceListMissingError(frappe.ValidationError):
- pass
-
-
-def set_cart_count(quotation=None):
- if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
- if not quotation:
- quotation = _get_cart_quotation()
- cart_count = cstr(cint(quotation.get("total_qty")))
-
- 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("billing", addresses[0].name)
-
- return {
- "doc": decorate_quotation_doc(doc),
- "shipping_addresses": get_shipping_addresses(party),
- "billing_addresses": get_billing_addresses(party),
- "shipping_rules": get_applicable_shipping_rules(party),
- "cart_settings": frappe.get_cached_doc("E Commerce Settings"),
- }
-
-
-@frappe.whitelist()
-def get_shipping_addresses(party=None):
- if not party:
- party = get_party()
- addresses = get_address_docs(party=party)
- return [
- {"name": address.name, "title": address.address_title, "display": address.display}
- for address in addresses
- if address.address_type == "Shipping"
- ]
-
-
-@frappe.whitelist()
-def get_billing_addresses(party=None):
- if not party:
- party = get_party()
- addresses = get_address_docs(party=party)
- return [
- {"name": address.name, "title": address.address_title, "display": address.display}
- for address in addresses
- if address.address_type == "Billing"
- ]
-
-
-@frappe.whitelist()
-def place_order():
- quotation = _get_cart_quotation()
- cart_settings = frappe.db.get_value(
- "E Commerce Settings", None, ["company", "allow_items_not_in_stock"], as_dict=1
- )
- quotation.company = cart_settings.company
-
- quotation.flags.ignore_permissions = True
- quotation.submit()
-
- if quotation.quotation_to == "Lead" and quotation.party_name:
- # company used to create customer accounts
- frappe.defaults.set_user_default("company", quotation.company)
-
- if not (quotation.shipping_address_name or quotation.customer_address):
- frappe.throw(_("Set Shipping Address or Billing Address"))
-
- from erpnext.selling.doctype.quotation.quotation import _make_sales_order
-
- sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True))
- sales_order.payment_schedule = []
-
- if not cint(cart_settings.allow_items_not_in_stock):
- for item in sales_order.get("items"):
- item.warehouse = frappe.db.get_value(
- "Website Item", {"item_code": item.item_code}, "website_warehouse"
- )
- is_stock_item = frappe.db.get_value("Item", item.item_code, "is_stock_item")
-
- if is_stock_item:
- item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
- if not cint(item_stock.in_stock):
- throw(_("{0} Not in Stock").format(item.item_code))
- if item.qty > item_stock.stock_qty:
- throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, 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 request_for_quotation():
- quotation = _get_cart_quotation()
- quotation.flags.ignore_permissions = True
-
- if get_shopping_cart_settings().save_quotations_as_draft:
- quotation.save()
- else:
- quotation.submit()
- return quotation.name
-
-
-@frappe.whitelist()
-def update_cart(item_code, qty, additional_notes=None, 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:
- warehouse = frappe.get_cached_value(
- "Website Item", {"item_code": item_code}, "website_warehouse"
- )
-
- 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,
- "additional_notes": additional_notes,
- "warehouse": warehouse,
- },
- )
- else:
- quotation_items[0].qty = qty
- quotation_items[0].additional_notes = additional_notes
- quotation_items[0].warehouse = warehouse
-
- 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)
-
- if cint(with_items):
- context = get_cart_quotation(quotation)
- return {
- "items": frappe.render_template("templates/includes/cart/cart_items.html", context),
- "total": frappe.render_template("templates/includes/cart/cart_items_total.html", context),
- "taxes_and_totals": frappe.render_template(
- "templates/includes/cart/cart_payment_summary.html", context
- ),
- }
- else:
- return {"name": quotation.name}
-
-
-@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 add_new_address(doc):
- doc = frappe.parse_json(doc)
- doc.update({"doctype": "Address"})
- address = frappe.get_doc(doc)
- address.save(ignore_permissions=True)
-
- return address
-
-
-@frappe.whitelist(allow_guest=True)
-def create_lead_for_item_inquiry(lead, subject, message):
- lead = frappe.parse_json(lead)
- lead_doc = frappe.new_doc("Lead")
- for fieldname in ("lead_name", "company_name", "email_id", "phone"):
- lead_doc.set(fieldname, lead.get(fieldname))
-
- lead_doc.set("lead_owner", "")
-
- if not frappe.db.exists("Lead Source", "Product Inquiry"):
- frappe.get_doc({"doctype": "Lead Source", "source_name": "Product Inquiry"}).insert(
- ignore_permissions=True
- )
-
- lead_doc.set("source", "Product Inquiry")
-
- try:
- lead_doc.save(ignore_permissions=True)
- except frappe.exceptions.DuplicateEntryError:
- frappe.clear_messages()
- lead_doc = frappe.get_doc("Lead", {"email_id": lead["email_id"]})
-
- lead_doc.add_comment(
- "Comment",
- text="""
-
-
{subject}
-
{message}
-
- """.format(
- subject=subject, message=message
- ),
- )
-
- return lead_doc
-
-
-@frappe.whitelist()
-def get_terms_and_conditions(terms_name):
- return frappe.db.get_value("Terms and Conditions", terms_name, "terms")
-
-
-@frappe.whitelist()
-def update_cart_address(address_type, address_name):
- quotation = _get_cart_quotation()
- address_doc = frappe.get_doc("Address", address_name).as_dict()
- address_display = get_address_display(address_doc)
-
- if address_type.lower() == "billing":
- quotation.customer_address = address_name
- quotation.address_display = address_display
- quotation.shipping_address_name = quotation.shipping_address_name or address_name
- address_doc = next((doc for doc in get_billing_addresses() if doc["name"] == address_name), None)
- elif address_type.lower() == "shipping":
- quotation.shipping_address_name = address_name
- quotation.shipping_address = address_display
- quotation.customer_address = quotation.customer_address or address_name
- address_doc = next(
- (doc for doc in get_shipping_addresses() if doc["name"] == address_name), None
- )
- apply_cart_settings(quotation=quotation)
-
- quotation.flags.ignore_permissions = True
- quotation.save()
-
- context = get_cart_quotation(quotation)
- context["address"] = address_doc
-
- return {
- "taxes": frappe.render_template("templates/includes/order/order_taxes.html", context),
- "address": frappe.render_template("templates/includes/cart/address_card.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("E Commerce Settings", None, "territory")
- or get_root_of("Territory")
- )
-
-
-def decorate_quotation_doc(doc):
- for d in doc.get("items", []):
- item_code = d.item_code
- fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
-
- # Variant Item
- if not frappe.db.exists("Website Item", {"item_code": item_code}):
- variant_data = frappe.db.get_values(
- "Item",
- filters={"item_code": item_code},
- fieldname=["variant_of", "item_name", "image"],
- as_dict=True,
- )[0]
- item_code = variant_data.variant_of
- fields = fields[1:]
- d.web_item_name = variant_data.item_name
-
- if variant_data.image: # get image from variant or template web item
- d.thumbnail = variant_data.image
- fields = fields[2:]
-
- d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True))
- website_warehouse = frappe.get_cached_value(
- "Website Item", {"item_code": item_code}, "website_warehouse"
- )
- d.warehouse = website_warehouse
-
- 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_name": party.name,
- "contact_email": frappe.session.user,
- "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:
- company = frappe.db.get_value("E Commerce Settings", None, ["company"])
- qdoc = frappe.get_doc(
- {
- "doctype": "Quotation",
- "naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
- "quotation_to": party.doctype,
- "company": company,
- "order_type": "Shopping Cart",
- "status": "Draft",
- "docstatus": 0,
- "__islocal": 1,
- "party_name": 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("E Commerce 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(cart_settings, quotation)
-
- # 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(cart_settings, quotation=None):
- """Set price list based on customer or shopping cart default"""
- from erpnext.accounts.party import get_default_price_list
-
- party_name = quotation.get("party_name") if quotation else get_party().get("name")
- selling_price_list = None
-
- # check if default customer price list exists
- if party_name and frappe.db.exists("Customer", party_name):
- selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name))
-
- # check default price list in shopping cart
- if not selling_price_list:
- selling_price_list = cart_settings.price_list
-
- if quotation:
- quotation.selling_price_list = selling_price_list
-
- return 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.party_name, "customer_group")
-
- quotation.taxes_and_charges = set_taxes(
- quotation.party_name,
- "Customer",
- quotation.transaction_date,
- quotation.company,
- customer_group=customer_group,
- supplier_group=None,
- tax_category=quotation.tax_category,
- billing_address=quotation.customer_address,
- shipping_address=quotation.shipping_address_name,
- use_for_shopping_cart=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 = get_contact_name(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("E Commerce 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"),
- }
- )
-
- customer.append("portal_users", {"user": user})
-
- 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_ids": [{"email_id": user, "is_primary": 1}]})
- 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):
- if not cart_settings.payment_gateway_account:
- frappe.throw(_("Payment Gateway Account not set"), _("Mandatory"))
-
- 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:
- # we need this in sorted order as per the position of the rule in the settings page
- return [[rule, 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:
- sr_country = frappe.qb.DocType("Shipping Rule Country")
- sr = frappe.qb.DocType("Shipping Rule")
- query = (
- frappe.qb.from_(sr_country)
- .join(sr)
- .on(sr.name == sr_country.parent)
- .select(sr.name)
- .distinct()
- .where((sr_country.country == country) & (sr.disabled != 1))
- )
- result = query.run(as_list=True)
- shipping_rules = [x[0] for x in result]
-
- 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
-
-
-@frappe.whitelist(allow_guest=True)
-def apply_coupon_code(applied_code, applied_referral_sales_partner):
- quotation = True
-
- if not applied_code:
- frappe.throw(_("Please enter a coupon code"))
-
- coupon_list = frappe.get_all("Coupon Code", filters={"coupon_code": applied_code})
- if not coupon_list:
- frappe.throw(_("Please enter a valid coupon code"))
-
- coupon_name = coupon_list[0].name
-
- from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
-
- validate_coupon_code(coupon_name)
- quotation = _get_cart_quotation()
- quotation.coupon_code = coupon_name
- quotation.flags.ignore_permissions = True
- quotation.save()
-
- if applied_referral_sales_partner:
- sales_partner_list = frappe.get_all(
- "Sales Partner", filters={"referral_code": applied_referral_sales_partner}
- )
- if sales_partner_list:
- sales_partner_name = sales_partner_list[0].name
- quotation.referral_sales_partner = sales_partner_name
- quotation.flags.ignore_permissions = True
- quotation.save()
-
- return quotation
diff --git a/erpnext/e_commerce/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py
deleted file mode 100644
index 0248ca73d7..0000000000
--- a/erpnext/e_commerce/shopping_cart/product_info.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-
-from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
- get_shopping_cart_settings,
- show_quantity_in_website,
-)
-from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list
-from erpnext.utilities.product import (
- get_non_stock_item_status,
- get_price,
- get_web_item_qty_in_stock,
-)
-
-
-@frappe.whitelist(allow_guest=True)
-def get_product_info_for_website(item_code, skip_quotation_creation=False):
- """get product price / stock info for website"""
-
- cart_settings = get_shopping_cart_settings()
- if not cart_settings.enabled:
- # return settings even if cart is disabled
- return frappe._dict({"product_info": {}, "cart_settings": cart_settings})
-
- cart_quotation = frappe._dict()
- if not skip_quotation_creation:
- cart_quotation = _get_cart_quotation()
-
- selling_price_list = (
- cart_quotation.get("selling_price_list")
- if cart_quotation
- else _set_price_list(cart_settings, None)
- )
-
- price = {}
- if cart_settings.show_price:
- is_guest = frappe.session.user == "Guest"
- # Show Price if logged in.
- # If not logged in, check if price is hidden for guest.
- if not is_guest or not cart_settings.hide_price_for_guest:
- price = get_price(
- item_code, selling_price_list, cart_settings.default_customer_group, cart_settings.company
- )
-
- stock_status = None
-
- if cart_settings.show_stock_availability:
- on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder")
- if on_backorder:
- stock_status = frappe._dict({"on_backorder": True})
- else:
- stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse")
-
- product_info = {
- "price": price,
- "qty": 0,
- "uom": frappe.db.get_value("Item", item_code, "stock_uom"),
- "sales_uom": frappe.db.get_value("Item", item_code, "sales_uom"),
- }
-
- if stock_status:
- if stock_status.on_backorder:
- product_info["on_backorder"] = True
- else:
- product_info["stock_qty"] = stock_status.stock_qty
- product_info["in_stock"] = (
- stock_status.in_stock
- if stock_status.is_stock_item
- else get_non_stock_item_status(item_code, "website_warehouse")
- )
- product_info["show_stock_qty"] = show_quantity_in_website()
-
- if product_info["price"]:
- if frappe.session.user != "Guest":
- item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
- if item:
- product_info["qty"] = item[0].qty
-
- return frappe._dict({"product_info": product_info, "cart_settings": cart_settings})
-
-
-def set_product_info_for_website(item):
- """set product price uom for website"""
- product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get(
- "product_info"
- )
-
- if product_info:
- item.update(product_info)
- item["stock_uom"] = product_info.get("uom")
- item["sales_uom"] = product_info.get("sales_uom")
- if product_info.get("price"):
- item["price_stock_uom"] = product_info.get("price").get("formatted_price")
- item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom")
- else:
- item["price_stock_uom"] = ""
- item["price_sales_uom"] = ""
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
deleted file mode 100644
index 8210f9743d..0000000000
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ /dev/null
@@ -1,398 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import unittest
-
-import frappe
-from frappe.tests.utils import change_settings
-from frappe.utils import add_months, cint, nowdate
-
-from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
-from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
-from erpnext.e_commerce.shopping_cart.cart import (
- _get_cart_quotation,
- get_cart_quotation,
- get_party,
- request_for_quotation,
- update_cart,
-)
-
-
-class TestShoppingCart(unittest.TestCase):
- """
- Note:
- Shopping Cart == Quotation
- """
-
- def setUp(self):
- frappe.set_user("Administrator")
- self.enable_shopping_cart()
- if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
- make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
-
- if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}):
- make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
-
- def tearDown(self):
- frappe.db.rollback()
- frappe.set_user("Administrator")
- self.disable_shopping_cart()
-
- @classmethod
- def tearDownClass(cls):
- frappe.db.sql("delete from `tabTax Rule`")
-
- def test_get_cart_new_user(self):
- self.login_as_customer(
- "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
- )
- create_address_and_contact(
- address_title="_Test Address for Customer 2",
- first_name="_Test Contact for Customer 2",
- email="test_contact_two_customer@example.com",
- customer="_Test Customer 2",
- )
- # test if lead is created and quotation with new lead is fetched
- customer = frappe.get_doc("Customer", "_Test Customer 2")
- quotation = _get_cart_quotation(party=customer)
- self.assertEqual(quotation.quotation_to, "Customer")
- self.assertEqual(
- quotation.contact_person,
- frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
- )
- self.assertEqual(quotation.contact_email, frappe.session.user)
-
- return quotation
-
- def test_get_cart_customer(self, customer="_Test Customer 2"):
- def validate_quotation(customer_name):
- # test if quotation with customer is fetched
- party = frappe.get_doc("Customer", customer_name)
- quotation = _get_cart_quotation(party=party)
- self.assertEqual(quotation.quotation_to, "Customer")
- self.assertEqual(quotation.party_name, customer_name)
- self.assertEqual(quotation.contact_email, frappe.session.user)
- return quotation
-
- quotation = validate_quotation(customer)
- return quotation
-
- def test_add_to_cart(self):
- self.login_as_customer(
- "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
- )
- create_address_and_contact(
- address_title="_Test Address for Customer 2",
- first_name="_Test Contact for Customer 2",
- email="test_contact_two_customer@example.com",
- customer="_Test Customer 2",
- )
- # clear existing quotations
- self.clear_existing_quotations()
-
- # add first item
- update_cart("_Test Item", 1)
-
- quotation = self.test_get_cart_customer("_Test Customer 2")
-
- self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
- self.assertEqual(quotation.get("items")[0].qty, 1)
- self.assertEqual(quotation.get("items")[0].amount, 10)
-
- # add second item
- update_cart("_Test Item 2", 1)
- quotation = self.test_get_cart_customer("_Test Customer 2")
- self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
- self.assertEqual(quotation.get("items")[1].qty, 1)
- self.assertEqual(quotation.get("items")[1].amount, 20)
-
- self.assertEqual(len(quotation.get("items")), 2)
-
- def test_update_cart(self):
- # first, add to cart
- self.test_add_to_cart()
-
- # update first item
- update_cart("_Test Item", 5)
- quotation = self.test_get_cart_customer("_Test Customer 2")
- self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
- self.assertEqual(quotation.get("items")[0].qty, 5)
- self.assertEqual(quotation.get("items")[0].amount, 50)
- self.assertEqual(quotation.net_total, 70)
- self.assertEqual(len(quotation.get("items")), 2)
-
- def test_remove_from_cart(self):
- # first, add to cart
- self.test_add_to_cart()
-
- # remove first item
- update_cart("_Test Item", 0)
- quotation = self.test_get_cart_customer("_Test Customer 2")
-
- self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
- self.assertEqual(quotation.get("items")[0].qty, 1)
- self.assertEqual(quotation.get("items")[0].amount, 20)
- self.assertEqual(quotation.net_total, 20)
- self.assertEqual(len(quotation.get("items")), 1)
-
- @unittest.skip("Flaky in CI")
- def test_tax_rule(self):
- self.create_tax_rule()
-
- self.login_as_customer(
- "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
- )
- create_address_and_contact(
- address_title="_Test Address for Customer 2",
- first_name="_Test Contact for Customer 2",
- email="test_contact_two_customer@example.com",
- customer="_Test Customer 2",
- )
-
- quotation = self.create_quotation()
-
- from erpnext.accounts.party import set_taxes
-
- tax_rule_master = set_taxes(
- quotation.party_name,
- "Customer",
- None,
- quotation.company,
- customer_group=None,
- supplier_group=None,
- tax_category=quotation.tax_category,
- billing_address=quotation.customer_address,
- shipping_address=quotation.shipping_address_name,
- use_for_shopping_cart=1,
- )
-
- self.assertEqual(quotation.taxes_and_charges, tax_rule_master)
- self.assertEqual(quotation.total_taxes_and_charges, 1000.0)
-
- self.remove_test_quotation(quotation)
-
- @change_settings(
- "E Commerce Settings",
- {
- "company": "_Test Company",
- "enabled": 1,
- "default_customer_group": "_Test Customer Group",
- "price_list": "_Test Price List India",
- "show_price": 1,
- },
- )
- def test_add_item_variant_without_web_item_to_cart(self):
- "Test adding Variants having no Website Items in cart via Template Web Item."
- from erpnext.controllers.item_variant import create_variant
- from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
- from erpnext.stock.doctype.item.test_item import make_item
-
- template_item = make_item(
- "Test-Tshirt-Temp",
- {
- "has_variant": 1,
- "variant_based_on": "Item Attribute",
- "attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}],
- },
- )
- variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"})
- variant.save()
- make_website_item(template_item) # publish template not variant
-
- update_cart("Test-Tshirt-Temp-S-R", 1)
-
- cart = get_cart_quotation() # test if cart page gets data without errors
- doc = cart.get("doc")
-
- self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
-
- # test if items are rendered without error
- frappe.render_template("templates/includes/cart/cart_items.html", cart)
-
- @change_settings("E Commerce Settings", {"save_quotations_as_draft": 1})
- def test_cart_without_checkout_and_draft_quotation(self):
- "Test impact of 'save_quotations_as_draft' checkbox."
- frappe.local.shopping_cart_settings = None
-
- # add item to cart
- update_cart("_Test Item", 1)
- quote_name = request_for_quotation() # Request for Quote
- quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
-
- self.assertEqual(quote_doctstatus, 0)
-
- frappe.db.set_single_value("E Commerce Settings", "save_quotations_as_draft", 0)
- frappe.local.shopping_cart_settings = None
- update_cart("_Test Item", 1)
- quote_name = request_for_quotation() # Request for Quote
- quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
-
- self.assertEqual(quote_doctstatus, 1)
-
- def create_tax_rule(self):
- tax_rule = frappe.get_test_records("Tax Rule")[0]
- try:
- frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)
- except (frappe.DuplicateEntryError, ConflictingTaxRule):
- pass
-
- def create_quotation(self):
- quotation = frappe.new_doc("Quotation")
-
- values = {
- "doctype": "Quotation",
- "quotation_to": "Customer",
- "order_type": "Shopping Cart",
- "party_name": get_party(frappe.session.user).name,
- "docstatus": 0,
- "contact_email": frappe.session.user,
- "selling_price_list": "_Test Price List Rest of the World",
- "currency": "USD",
- "taxes_and_charges": "_Test Tax 1 - _TC",
- "conversion_rate": 1,
- "transaction_date": nowdate(),
- "valid_till": add_months(nowdate(), 1),
- "items": [{"item_code": "_Test Item", "qty": 1}],
- "taxes": frappe.get_doc("Sales Taxes and Charges Template", "_Test Tax 1 - _TC").taxes,
- "company": "_Test Company",
- }
-
- quotation.update(values)
-
- quotation.insert(ignore_permissions=True)
-
- return quotation
-
- def remove_test_quotation(self, quotation):
- frappe.set_user("Administrator")
- quotation.delete()
-
- # helper functions
- def enable_shopping_cart(self):
- settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
-
- settings.update(
- {
- "enabled": 1,
- "company": "_Test Company",
- "default_customer_group": "_Test Customer Group",
- "quotation_series": "_T-Quotation-",
- "price_list": "_Test Price List India",
- }
- )
-
- # insert item price
- if not frappe.db.get_value(
- "Item Price", {"price_list": "_Test Price List India", "item_code": "_Test Item"}
- ):
- frappe.get_doc(
- {
- "doctype": "Item Price",
- "price_list": "_Test Price List India",
- "item_code": "_Test Item",
- "price_list_rate": 10,
- }
- ).insert()
- frappe.get_doc(
- {
- "doctype": "Item Price",
- "price_list": "_Test Price List India",
- "item_code": "_Test Item 2",
- "price_list_rate": 20,
- }
- ).insert()
-
- settings.save()
- frappe.local.shopping_cart_settings = None
-
- def disable_shopping_cart(self):
- settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
- settings.enabled = 0
- settings.save()
- frappe.local.shopping_cart_settings = None
-
- def login_as_new_user(self):
- self.create_user_if_not_exists("test_cart_user@example.com")
- frappe.set_user("test_cart_user@example.com")
-
- def login_as_customer(
- self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"
- ):
- self.create_user_if_not_exists(email, name)
- frappe.set_user(email)
-
- def clear_existing_quotations(self):
- quotations = frappe.get_all(
- "Quotation",
- filters={"party_name": get_party().name, "order_type": "Shopping Cart", "docstatus": 0},
- order_by="modified desc",
- pluck="name",
- )
-
- for quotation in quotations:
- frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True)
-
- def create_user_if_not_exists(self, email, first_name=None):
- if frappe.db.exists("User", email):
- return
-
- user = frappe.get_doc(
- {
- "doctype": "User",
- "user_type": "Website User",
- "email": email,
- "send_welcome_email": 0,
- "first_name": first_name or email.split("@")[0],
- }
- ).insert(ignore_permissions=True)
-
- user.add_roles("Customer")
-
-
-def create_address_and_contact(**kwargs):
- if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
- frappe.get_doc(
- {
- "doctype": "Address",
- "address_title": kwargs.get("address_title"),
- "address_type": kwargs.get("address_type") or "Office",
- "address_line1": kwargs.get("address_line1") or "Station Road",
- "city": kwargs.get("city") or "_Test City",
- "state": kwargs.get("state") or "Test State",
- "country": kwargs.get("country") or "India",
- "links": [
- {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
- ],
- }
- ).insert()
-
- if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
- contact = frappe.get_doc(
- {
- "doctype": "Contact",
- "first_name": kwargs.get("first_name"),
- "links": [
- {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
- ],
- }
- )
- contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
- contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
- contact.insert()
-
-
-test_dependencies = [
- "Sales Taxes and Charges Template",
- "Price List",
- "Item Price",
- "Shipping Rule",
- "Currency Exchange",
- "Customer Group",
- "Lead",
- "Customer",
- "Contact",
- "Address",
- "Item",
- "Tax Rule",
-]
diff --git a/erpnext/e_commerce/shopping_cart/utils.py b/erpnext/e_commerce/shopping_cart/utils.py
deleted file mode 100644
index 3d48c28dd1..0000000000
--- a/erpnext/e_commerce/shopping_cart/utils.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-import frappe
-
-from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
-
-
-def show_cart_count():
- if (
- is_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):
- # since this is run only on hooks login event
- # make sure user is already a customer
- # before trying to set cart count
- user_is_customer = is_customer()
- if not user_is_customer:
- return
-
- if show_cart_count():
- from erpnext.e_commerce.shopping_cart.cart import set_cart_count
-
- # set_cart_count will try to fetch existing cart quotation
- # or create one if non existent (and create a customer too)
- # cart count is calculated from this quotation's items
- 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):
- cart_enabled = is_cart_enabled()
- context["shopping_cart_enabled"] = cart_enabled
-
-
-def is_customer():
- if frappe.session.user and frappe.session.user != "Guest":
- contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
- if contact_name:
- contact = frappe.get_doc("Contact", contact_name)
- for link in contact.links:
- if link.link_doctype == "Customer":
- return True
-
- return False
diff --git a/erpnext/e_commerce/variant_selector/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
deleted file mode 100644
index f8439d5d43..0000000000
--- a/erpnext/e_commerce/variant_selector/item_variants_cache.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import frappe
-
-
-class ItemVariantsCacheManager:
- def __init__(self, item_code):
- self.item_code = item_code
-
- def get_item_variants_data(self):
- val = frappe.cache().hget("item_variants_data", self.item_code)
-
- if not val:
- self.build_cache()
-
- return frappe.cache().hget("item_variants_data", self.item_code)
-
- def get_attribute_value_item_map(self):
- val = frappe.cache().hget("attribute_value_item_map", self.item_code)
-
- if not val:
- self.build_cache()
-
- return frappe.cache().hget("attribute_value_item_map", self.item_code)
-
- def get_item_attribute_value_map(self):
- val = frappe.cache().hget("item_attribute_value_map", self.item_code)
-
- if not val:
- self.build_cache()
-
- return frappe.cache().hget("item_attribute_value_map", self.item_code)
-
- def get_optional_attributes(self):
- val = frappe.cache().hget("optional_attributes", self.item_code)
-
- if not val:
- self.build_cache()
-
- return frappe.cache().hget("optional_attributes", self.item_code)
-
- def get_ordered_attribute_values(self):
- val = frappe.cache().get_value("ordered_attribute_values_map")
- if val:
- return val
-
- all_attribute_values = frappe.get_all(
- "Item Attribute Value", ["attribute_value", "idx", "parent"], order_by="idx asc"
- )
-
- ordered_attribute_values_map = frappe._dict({})
- for d in all_attribute_values:
- ordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value)
-
- frappe.cache().set_value("ordered_attribute_values_map", ordered_attribute_values_map)
- return ordered_attribute_values_map
-
- def build_cache(self):
- parent_item_code = self.item_code
-
- attributes = [
- a.attribute
- for a in frappe.get_all(
- "Item Variant Attribute", {"parent": parent_item_code}, ["attribute"], order_by="idx asc"
- )
- ]
-
- # Get Variants and tehir Attributes that are not disabled
- iva = frappe.qb.DocType("Item Variant Attribute")
- item = frappe.qb.DocType("Item")
- query = (
- frappe.qb.from_(iva)
- .join(item)
- .on(item.name == iva.parent)
- .select(iva.parent, iva.attribute, iva.attribute_value)
- .where((iva.variant_of == parent_item_code) & (item.disabled == 0))
- .orderby(iva.name)
- )
- item_variants_data = query.run()
-
- attribute_value_item_map = frappe._dict()
- item_attribute_value_map = frappe._dict()
-
- for row in item_variants_data:
- item_code, attribute, attribute_value = row
- # (attr, value) => [item1, item2]
- attribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code)
- # item => {attr1: value1, attr2: value2}
- item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value
-
- optional_attributes = set()
- for item_code, attr_dict in item_attribute_value_map.items():
- for attribute in attributes:
- if attribute not in attr_dict:
- optional_attributes.add(attribute)
-
- frappe.cache().hset("attribute_value_item_map", parent_item_code, attribute_value_item_map)
- frappe.cache().hset("item_attribute_value_map", parent_item_code, item_attribute_value_map)
- frappe.cache().hset("item_variants_data", parent_item_code, item_variants_data)
- frappe.cache().hset("optional_attributes", parent_item_code, optional_attributes)
-
- def clear_cache(self):
- keys = [
- "attribute_value_item_map",
- "item_attribute_value_map",
- "item_variants_data",
- "optional_attributes",
- ]
-
- for key in keys:
- frappe.cache().hdel(key, self.item_code)
-
- def rebuild_cache(self):
- self.clear_cache()
- enqueue_build_cache(self.item_code)
-
-
-def build_cache(item_code):
- frappe.cache().hset("item_cache_build_in_progress", item_code, 1)
- i = ItemVariantsCacheManager(item_code)
- i.build_cache()
- frappe.cache().hset("item_cache_build_in_progress", item_code, 0)
-
-
-def enqueue_build_cache(item_code):
- if frappe.cache().hget("item_cache_build_in_progress", item_code):
- return
- frappe.enqueue(
- "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
- item_code=item_code,
- queue="long",
- )
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
deleted file mode 100644
index 8eb497c1b5..0000000000
--- a/erpnext/e_commerce/variant_selector/test_variant_selector.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import frappe
-from frappe.tests.utils import FrappeTestCase
-
-from erpnext.controllers.item_variant import create_variant
-from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
- setup_e_commerce_settings,
-)
-from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
-from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
-from erpnext.stock.doctype.item.test_item import make_item
-
-test_dependencies = ["Item"]
-
-
-class TestVariantSelector(FrappeTestCase):
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- template_item = make_item(
- "Test-Tshirt-Temp",
- {
- "has_variant": 1,
- "variant_based_on": "Item Attribute",
- "attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}],
- },
- )
-
- # create L-R, L-G, M-R, M-G and S-R
- for size in (
- "Large",
- "Medium",
- ):
- for colour in (
- "Red",
- "Green",
- ):
- variant = create_variant("Test-Tshirt-Temp", {"Test Size": size, "Test Colour": colour})
- variant.save()
-
- variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"})
- variant.save()
-
- make_website_item(template_item) # publish template not variants
-
- def test_item_attributes(self):
- """
- Test if the right attributes are fetched in the popup.
- (Attributes must only come from active items)
-
- Attribute selection must not be linked to Website Items.
- """
- from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
-
- attr_data = get_attributes_and_values("Test-Tshirt-Temp")
-
- self.assertEqual(attr_data[0]["attribute"], "Test Size")
- self.assertEqual(attr_data[1]["attribute"], "Test Colour")
- self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
- self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
-
- # disable small red tshirt, now there are no small tshirts.
- # but there are some red tshirts
- small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
- small_variant.disabled = 1
- small_variant.save() # trigger cache rebuild
-
- attr_data = get_attributes_and_values("Test-Tshirt-Temp")
-
- # Only L and M attribute values must be fetched since S is disabled
- self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
-
- # teardown
- small_variant.disabled = 0
- small_variant.save()
-
- def test_next_item_variant_values(self):
- """
- Test if on selecting an attribute value, the next possible values
- are filtered accordingly.
- Values that dont apply should not be fetched.
- E.g.
- There is a ** Small-Red ** Tshirt. No other colour in this size.
- On selecting ** Small **, only ** Red ** should be selectable next.
- """
- next_values = get_next_attribute_and_values(
- "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"}
- )
- next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
- filtered_items = next_values["filtered_items"]
-
- self.assertEqual(len(next_colours), 1)
- self.assertEqual(next_colours.pop(), "Red")
- self.assertEqual(len(filtered_items), 1)
- self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
-
- def test_exact_match_with_price(self):
- """
- Test price fetching and matching of variant without Website Item
- """
- from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
-
- frappe.set_user("Administrator")
- setup_e_commerce_settings(
- {
- "company": "_Test Company",
- "enabled": 1,
- "default_customer_group": "_Test Customer Group",
- "price_list": "_Test Price List India",
- "show_price": 1,
- }
- )
-
- make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
-
- frappe.local.shopping_cart_settings = None # clear cached settings values
- next_values = get_next_attribute_and_values(
- "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
- )
- print(">>>>", next_values)
- price_info = next_values["product_info"]["price"]
-
- self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R")
- self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R")
- self.assertEqual(price_info["price_list_rate"], 100.0)
- self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py
deleted file mode 100644
index 88356f5e90..0000000000
--- a/erpnext/e_commerce/variant_selector/utils.py
+++ /dev/null
@@ -1,251 +0,0 @@
-import frappe
-from frappe.utils import cint, flt
-
-from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
- get_shopping_cart_settings,
-)
-from erpnext.e_commerce.shopping_cart.cart import _set_price_list
-from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
-from erpnext.utilities.product import get_price
-
-
-def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
- items = []
-
- for attribute, values in attribute_filters.items():
- attribute_values = values
-
- if not isinstance(attribute_values, list):
- attribute_values = [attribute_values]
-
- if not attribute_values:
- continue
-
- wheres = []
- query_values = []
- for attribute_value in attribute_values:
- wheres.append("( attribute = %s and attribute_value = %s )")
- query_values += [attribute, attribute_value]
-
- attribute_query = " or ".join(wheres)
-
- if template_item_code:
- variant_of_query = "AND t2.variant_of = %s"
- query_values.append(template_item_code)
- else:
- variant_of_query = ""
-
- query = """
- SELECT
- t1.parent
- FROM
- `tabItem Variant Attribute` t1
- WHERE
- 1 = 1
- AND (
- {attribute_query}
- )
- AND EXISTS (
- SELECT
- 1
- FROM
- `tabItem` t2
- WHERE
- t2.name = t1.parent
- {variant_of_query}
- )
- GROUP BY
- t1.parent
- ORDER BY
- NULL
- """.format(
- attribute_query=attribute_query, variant_of_query=variant_of_query
- )
-
- item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) # nosemgrep
- items.append(item_codes)
-
- res = list(set.intersection(*items))
-
- return res
-
-
-@frappe.whitelist(allow_guest=True)
-def get_attributes_and_values(item_code):
- """Build a list of attributes and their possible values.
- This will ignore the values upon selection of which there cannot exist one item.
- """
- item_cache = ItemVariantsCacheManager(item_code)
- item_variants_data = item_cache.get_item_variants_data()
-
- attributes = get_item_attributes(item_code)
- attribute_list = [a.attribute for a in attributes]
-
- valid_options = {}
- for item_code, attribute, attribute_value in item_variants_data:
- if attribute in attribute_list:
- valid_options.setdefault(attribute, set()).add(attribute_value)
-
- item_attribute_values = frappe.db.get_all(
- "Item Attribute Value", ["parent", "attribute_value", "idx"], order_by="parent asc, idx asc"
- )
- ordered_attribute_value_map = frappe._dict()
- for iv in item_attribute_values:
- ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)
-
- # build attribute values in idx order
- for attr in attributes:
- valid_attribute_values = valid_options.get(attr.attribute, [])
- ordered_values = ordered_attribute_value_map.get(attr.attribute, [])
- attr["values"] = [v for v in ordered_values if v in valid_attribute_values]
-
- return attributes
-
-
-@frappe.whitelist(allow_guest=True)
-def get_next_attribute_and_values(item_code, selected_attributes):
- from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
-
- """Find the count of Items that match the selected attributes.
- Also, find the attribute values that are not applicable for further searching.
- If less than equal to 10 items are found, return item_codes of those items.
- If one item is matched exactly, return item_code of that item.
- """
- selected_attributes = frappe.parse_json(selected_attributes)
-
- item_cache = ItemVariantsCacheManager(item_code)
- item_variants_data = item_cache.get_item_variants_data()
-
- attributes = get_item_attributes(item_code)
- attribute_list = [a.attribute for a in attributes]
- filtered_items = get_items_with_selected_attributes(item_code, selected_attributes)
-
- next_attribute = None
-
- for attribute in attribute_list:
- if attribute not in selected_attributes:
- next_attribute = attribute
- break
-
- valid_options_for_attributes = frappe._dict()
-
- for a in attribute_list:
- valid_options_for_attributes[a] = set()
-
- selected_attribute = selected_attributes.get(a, None)
- if selected_attribute:
- # already selected attribute values are valid options
- valid_options_for_attributes[a].add(selected_attribute)
-
- for row in item_variants_data:
- item_code, attribute, attribute_value = row
- if (
- item_code in filtered_items
- and attribute not in selected_attributes
- and attribute in attribute_list
- ):
- valid_options_for_attributes[attribute].add(attribute_value)
-
- optional_attributes = item_cache.get_optional_attributes()
- exact_match = []
- # search for exact match if all selected attributes are required attributes
- if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
- item_attribute_value_map = item_cache.get_item_attribute_value_map()
- for item_code, attr_dict in item_attribute_value_map.items():
- if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):
- exact_match.append(item_code)
-
- filtered_items_count = len(filtered_items)
-
- # get product info if exact match
- # from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
- if exact_match:
- cart_settings = get_shopping_cart_settings()
- product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
-
- if product_info:
- product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item")
- product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
- else:
- product_info = None
-
- product_id = ""
- warehouse = ""
- if exact_match or filtered_items:
- if exact_match and len(exact_match) == 1:
- product_id = exact_match[0]
- elif filtered_items_count == 1:
- product_id = list(filtered_items)[0]
-
- if product_id:
- warehouse = frappe.get_cached_value(
- "Website Item", {"item_code": product_id}, "website_warehouse"
- )
-
- available_qty = 0.0
- if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
- warehouses = get_child_warehouses(warehouse)
- else:
- warehouses = [warehouse] if warehouse else []
-
- for warehouse in warehouses:
- available_qty += flt(
- frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty")
- )
-
- return {
- "next_attribute": next_attribute,
- "valid_options_for_attributes": valid_options_for_attributes,
- "filtered_items_count": filtered_items_count,
- "filtered_items": filtered_items if filtered_items_count < 10 else [],
- "exact_match": exact_match,
- "product_info": product_info,
- "available_qty": available_qty,
- }
-
-
-def get_items_with_selected_attributes(item_code, selected_attributes):
- item_cache = ItemVariantsCacheManager(item_code)
- attribute_value_item_map = item_cache.get_attribute_value_item_map()
-
- items = []
- for attribute, value in selected_attributes.items():
- filtered_items = attribute_value_item_map.get((attribute, value), [])
- items.append(set(filtered_items))
-
- return set.intersection(*items)
-
-
-# utilities
-
-
-def get_item_attributes(item_code):
- attributes = frappe.db.get_all(
- "Item Variant Attribute",
- fields=["attribute"],
- filters={"parenttype": "Item", "parent": item_code},
- order_by="idx asc",
- )
-
- optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
-
- for a in attributes:
- if a.attribute in optional_attributes:
- a.optional = True
-
- return attributes
-
-
-def get_item_variant_price_dict(item_code, cart_settings):
- if cart_settings.enabled and cart_settings.show_price:
- is_guest = frappe.session.user == "Guest"
- # Show Price if logged in.
- # If not logged in, check if price is hidden for guest.
- if not is_guest or not cart_settings.hide_price_for_guest:
- price_list = _set_price_list(cart_settings, None)
- price = get_price(
- item_code, price_list, cart_settings.default_customer_group, cart_settings.company
- )
- return {"price": price}
-
- return None
diff --git a/erpnext/e_commerce/web_template/__init__.py b/erpnext/e_commerce/web_template/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/web_template/hero_slider/__init__.py b/erpnext/e_commerce/web_template/hero_slider/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
deleted file mode 100644
index fe4fee375b..0000000000
--- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
+++ /dev/null
@@ -1,86 +0,0 @@
-{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
-{%- set align_class = resolve_class({
- 'text-right': align == 'Right',
- 'text-center': align == 'Centre',
- 'text-left': align == 'Left',
-}) -%}
-
-{%- set heading_class = resolve_class({
- 'text-white': theme == 'Dark',
- '': theme == 'Light',
-}) -%}
-
-
- {%- if title or subtitle -%}
-
-
- {%- if title -%}
{{ title }} {%- endif -%}
- {%- if subtitle -%}
{{ subtitle }}
{%- endif -%}
- {%- if action -%}
-
- {{ label }}
-
- {%- endif -%}
-
-
- {%- endif -%}
-
-{%- endmacro -%}
-
-{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%}
-
-
- {%- if show_indicators -%}
-
- {%- for index in ['1', '2', '3', '4', '5'] -%}
- {%- if values['slide_' + index + '_image'] -%}
-
- {%- endif -%}
- {%- endfor -%}
-
- {%- endif -%}
-
- {%- for index in ['1', '2', '3', '4', '5'] -%}
- {%- set image = values['slide_' + index + '_image'] -%}
- {%- set title = values['slide_' + index + '_title'] -%}
- {%- set subtitle = values['slide_' + index + '_subtitle'] -%}
- {%- set primary_action = values['slide_' + index + '_primary_action'] -%}
- {%- set primary_action_label = values['slide_' + index + '_primary_action_label'] -%}
- {%- set align = values['slide_' + index + '_content_align'] -%}
- {%- set theme = values['slide_' + index + '_theme'] -%}
-
- {%- if image -%}
- {{ slide(image, title, subtitle, primary_action, primary_action_label, index, align, theme) }}
- {%- endif -%}
-
- {%- endfor -%}
-
- {%- if show_controls -%}
-
-
- Previous
-
-
-
- Next
-
- {%- endif -%}
-
-
-
diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json
deleted file mode 100644
index 39b2b3eaeb..0000000000
--- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json
+++ /dev/null
@@ -1,288 +0,0 @@
-{
- "__unsaved": 1,
- "creation": "2020-11-17 15:21:51.207221",
- "docstatus": 0,
- "doctype": "Web Template",
- "fields": [
- {
- "fieldname": "slider_name",
- "fieldtype": "Data",
- "label": "Slider Name",
- "reqd": 1
- },
- {
- "default": "1",
- "fieldname": "show_indicators",
- "fieldtype": "Check",
- "label": "Show Indicators",
- "reqd": 0
- },
- {
- "default": "1",
- "fieldname": "show_controls",
- "fieldtype": "Check",
- "label": "Show Controls",
- "reqd": 0
- },
- {
- "fieldname": "slide_1",
- "fieldtype": "Section Break",
- "label": "Slide 1",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_image",
- "fieldtype": "Attach Image",
- "label": "Image",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_subtitle",
- "fieldtype": "Small Text",
- "label": "Subtitle",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_primary_action_label",
- "fieldtype": "Data",
- "label": "Primary Action Label",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_primary_action",
- "fieldtype": "Data",
- "label": "Primary Action",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_content_align",
- "fieldtype": "Select",
- "label": "Content Align",
- "options": "Left\nCentre\nRight",
- "reqd": 0
- },
- {
- "fieldname": "slide_1_theme",
- "fieldtype": "Select",
- "label": "Slide Theme",
- "options": "Dark\nLight",
- "reqd": 0
- },
- {
- "fieldname": "slide_2",
- "fieldtype": "Section Break",
- "label": "Slide 2",
- "reqd": 0
- },
- {
- "fieldname": "slide_2_image",
- "fieldtype": "Attach Image",
- "label": "Image ",
- "reqd": 0
- },
- {
- "fieldname": "slide_2_title",
- "fieldtype": "Data",
- "label": "Title ",
- "reqd": 0
- },
- {
- "fieldname": "slide_2_subtitle",
- "fieldtype": "Small Text",
- "label": "Subtitle ",
- "reqd": 0
- },
- {
- "fieldname": "slide_2_primary_action_label",
- "fieldtype": "Data",
- "label": "Primary Action Label ",
- "reqd": 0
- },
- {
- "fieldname": "slide_2_primary_action",
- "fieldtype": "Data",
- "label": "Primary Action ",
- "reqd": 0
- },
- {
- "default": "Left",
- "fieldname": "slide_2_content_align",
- "fieldtype": "Select",
- "label": "Content Align",
- "options": "Left\nCentre\nRight",
- "reqd": 0
- },
- {
- "fieldname": "slide_2_theme",
- "fieldtype": "Select",
- "label": "Slide Theme",
- "options": "Dark\nLight",
- "reqd": 0
- },
- {
- "fieldname": "slide_3",
- "fieldtype": "Section Break",
- "label": "Slide 3",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_image",
- "fieldtype": "Attach Image",
- "label": "Image",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_subtitle",
- "fieldtype": "Small Text",
- "label": "Subtitle",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_primary_action_label",
- "fieldtype": "Data",
- "label": "Primary Action Label",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_primary_action",
- "fieldtype": "Data",
- "label": "Primary Action",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_content_align",
- "fieldtype": "Select",
- "label": "Content Align",
- "options": "Left\nCentre\nRight",
- "reqd": 0
- },
- {
- "fieldname": "slide_3_theme",
- "fieldtype": "Select",
- "label": "Slide Theme",
- "options": "Dark\nLight",
- "reqd": 0
- },
- {
- "fieldname": "slide_4",
- "fieldtype": "Section Break",
- "label": "Slide 4",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_image",
- "fieldtype": "Attach Image",
- "label": "Image",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_subtitle",
- "fieldtype": "Small Text",
- "label": "Subtitle",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_primary_action_label",
- "fieldtype": "Data",
- "label": "Primary Action Label",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_primary_action",
- "fieldtype": "Data",
- "label": "Primary Action",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_content_align",
- "fieldtype": "Select",
- "label": "Content Align",
- "options": "Left\nCentre\nRight",
- "reqd": 0
- },
- {
- "fieldname": "slide_4_theme",
- "fieldtype": "Select",
- "label": "Slide Theme",
- "options": "Dark\nLight",
- "reqd": 0
- },
- {
- "fieldname": "slide_5",
- "fieldtype": "Section Break",
- "label": "Slide 5",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_image",
- "fieldtype": "Attach Image",
- "label": "Image",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_subtitle",
- "fieldtype": "Small Text",
- "label": "Subtitle",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_primary_action_label",
- "fieldtype": "Data",
- "label": "Primary Action Label",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_primary_action",
- "fieldtype": "Data",
- "label": "Primary Action",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_content_align",
- "fieldtype": "Select",
- "label": "Content Align",
- "options": "Left\nCentre\nRight",
- "reqd": 0
- },
- {
- "fieldname": "slide_5_theme",
- "fieldtype": "Select",
- "label": "Slide Theme",
- "options": "Dark\nLight",
- "reqd": 0
- }
- ],
- "idx": 2,
- "modified": "2023-05-12 15:03:57.604060",
- "modified_by": "Administrator",
- "module": "E-commerce",
- "name": "Hero Slider",
- "owner": "Administrator",
- "standard": 1,
- "template": "",
- "type": "Section"
-}
\ No newline at end of file
diff --git a/erpnext/e_commerce/web_template/item_card_group/__init__.py b/erpnext/e_commerce/web_template/item_card_group/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/web_template/item_card_group/item_card_group.html b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html
deleted file mode 100644
index 07952f056a..0000000000
--- a/erpnext/e_commerce/web_template/item_card_group/item_card_group.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
-
-
-
-
-
- {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
- {%- set item = values['card_' + index + '_item'] -%}
- {%- if item -%}
- {%- set web_item = frappe.get_doc("Website Item", item) -%}
- {{ item_card(
- web_item, is_featured=values['card_' + index + '_featured'],
- is_full_width=True, align="Center"
- ) }}
- {%- endif -%}
- {%- endfor -%}
-
-
-
-
diff --git a/erpnext/e_commerce/web_template/item_card_group/item_card_group.json b/erpnext/e_commerce/web_template/item_card_group/item_card_group.json
deleted file mode 100644
index ad9e2a7b24..0000000000
--- a/erpnext/e_commerce/web_template/item_card_group/item_card_group.json
+++ /dev/null
@@ -1,270 +0,0 @@
-{
- "__unsaved": 1,
- "creation": "2020-11-17 15:35:05.285322",
- "docstatus": 0,
- "doctype": "Web Template",
- "fields": [
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 1
- },
- {
- "fieldname": "subtitle",
- "fieldtype": "Data",
- "label": "Subtitle",
- "reqd": 0
- },
- {
- "fieldname": "primary_action_label",
- "fieldtype": "Data",
- "label": "Primary Action Label",
- "reqd": 0
- },
- {
- "fieldname": "primary_action",
- "fieldtype": "Data",
- "label": "Primary Action",
- "reqd": 0
- },
- {
- "fieldname": "card_1",
- "fieldtype": "Section Break",
- "label": "Card 1",
- "reqd": 0
- },
- {
- "fieldname": "card_1_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_1_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_2",
- "fieldtype": "Section Break",
- "label": "Card 2",
- "reqd": 0
- },
- {
- "fieldname": "card_2_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_2_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_3",
- "fieldtype": "Section Break",
- "label": "Card 3",
- "options": "",
- "reqd": 0
- },
- {
- "fieldname": "card_3_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_3_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_4",
- "fieldtype": "Section Break",
- "label": "Card 4",
- "reqd": 0
- },
- {
- "fieldname": "card_4_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_4_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_5",
- "fieldtype": "Section Break",
- "label": "Card 5",
- "reqd": 0
- },
- {
- "fieldname": "card_5_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_5_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_6",
- "fieldtype": "Section Break",
- "label": "Card 6",
- "reqd": 0
- },
- {
- "fieldname": "card_6_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_6_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_7",
- "fieldtype": "Section Break",
- "label": "Card 7",
- "reqd": 0
- },
- {
- "fieldname": "card_7_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_7_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_8",
- "fieldtype": "Section Break",
- "label": "Card 8",
- "reqd": 0
- },
- {
- "fieldname": "card_8_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_8_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_9",
- "fieldtype": "Section Break",
- "label": "Card 9",
- "reqd": 0
- },
- {
- "fieldname": "card_9_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_9_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_10",
- "fieldtype": "Section Break",
- "label": "Card 10",
- "reqd": 0
- },
- {
- "fieldname": "card_10_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_10_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_11",
- "fieldtype": "Section Break",
- "label": "Card 11",
- "reqd": 0
- },
- {
- "fieldname": "card_11_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_11_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- },
- {
- "fieldname": "card_12",
- "fieldtype": "Section Break",
- "label": "Card 12",
- "reqd": 0
- },
- {
- "fieldname": "card_12_item",
- "fieldtype": "Link",
- "label": "Website Item",
- "options": "Website Item",
- "reqd": 0
- },
- {
- "fieldname": "card_12_featured",
- "fieldtype": "Check",
- "label": "Featured",
- "reqd": 0
- }
- ],
- "idx": 0,
- "modified": "2021-12-21 14:44:59.821335",
- "modified_by": "Administrator",
- "module": "E-commerce",
- "name": "Item Card Group",
- "owner": "Administrator",
- "standard": 1,
- "template": "",
- "type": "Section"
-}
\ No newline at end of file
diff --git a/erpnext/e_commerce/web_template/product_card/__init__.py b/erpnext/e_commerce/web_template/product_card/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/web_template/product_card/product_card.html b/erpnext/e_commerce/web_template/product_card/product_card.html
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/web_template/product_card/product_card.json b/erpnext/e_commerce/web_template/product_card/product_card.json
deleted file mode 100644
index 2eb73741ef..0000000000
--- a/erpnext/e_commerce/web_template/product_card/product_card.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "__unsaved": 1,
- "creation": "2020-11-17 15:28:47.809342",
- "docstatus": 0,
- "doctype": "Web Template",
- "fields": [
- {
- "fieldname": "item",
- "fieldtype": "Link",
- "label": "Item",
- "options": "Item",
- "reqd": 0
- },
- {
- "fieldname": "featured",
- "fieldtype": "Check",
- "label": "Featured",
- "options": "",
- "reqd": 0
- }
- ],
- "idx": 0,
- "modified": "2021-02-24 16:05:17.926610",
- "modified_by": "Administrator",
- "module": "E-commerce",
- "name": "Product Card",
- "owner": "Administrator",
- "standard": 1,
- "template": "",
- "type": "Component"
-}
\ No newline at end of file
diff --git a/erpnext/e_commerce/web_template/product_category_cards/__init__.py b/erpnext/e_commerce/web_template/product_category_cards/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
deleted file mode 100644
index 6d75a8b1d5..0000000000
--- a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{%- macro card(title, image, url, text_primary=False) -%}
-{%- set align_class = resolve_class({
- 'text-right': text_primary,
- 'text-centre': align == 'Center',
- 'text-left': align == 'Left',
-}) -%}
-
- {% if image %}
-
- {% else %}
-
-
- {{ frappe.utils.get_abbr(title or '') }}
-
-
- {% endif %}
-
-
- {{ title or '' }}
-
-
-
-{%- endmacro -%}
-
-
- {%- if title -%}
-
{{ title }}
- {%- endif -%}
- {%- if subtitle -%}
-
{{ subtitle }}
- {%- endif -%}
-
-
-
- {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%}
- {%- set category = values['category_' + index] -%}
- {%- if category -%}
- {%- set category = frappe.get_doc("Item Group", category) -%}
- {{ card(category.name, category.image, category.route) }}
- {%- endif -%}
- {%- endfor -%}
-
-
-
-
-
diff --git a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
deleted file mode 100644
index 0202165d08..0000000000
--- a/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
+++ /dev/null
@@ -1,85 +0,0 @@
-{
- "__unsaved": 1,
- "creation": "2020-11-17 15:25:50.855934",
- "docstatus": 0,
- "doctype": "Web Template",
- "fields": [
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 1
- },
- {
- "fieldname": "subtitle",
- "fieldtype": "Data",
- "label": "Subtitle",
- "reqd": 0
- },
- {
- "fieldname": "category_1",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_2",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_3",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_4",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_5",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_6",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_7",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- },
- {
- "fieldname": "category_8",
- "fieldtype": "Link",
- "label": "Item Group",
- "options": "Item Group",
- "reqd": 0
- }
- ],
- "idx": 0,
- "modified": "2021-02-24 16:03:33.835635",
- "modified_by": "Administrator",
- "module": "E-commerce",
- "name": "Product Category Cards",
- "owner": "Administrator",
- "standard": 1,
- "template": "",
- "type": "Section"
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/__init__.py b/erpnext/erpnext_integrations/doctype/gocardless_mandate/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js b/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js
deleted file mode 100644
index 37f9f7b9df..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('GoCardless Mandate', {
-});
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json b/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json
deleted file mode 100644
index edf652c8f3..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.json
+++ /dev/null
@@ -1,184 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:mandate",
- "beta": 0,
- "creation": "2018-02-08 11:33:15.721919",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "disabled",
- "fieldtype": "Check",
- "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": "Disabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "options": "Customer",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "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,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mandate",
- "fieldtype": "Data",
- "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": "Mandate",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "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,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gocardless_customer",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "GoCardless Customer",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-11 12:28:03.183095",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "GoCardless Mandate",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py b/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py
deleted file mode 100644
index bceb3caebd..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/gocardless_mandate.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class GoCardlessMandate(Document):
- pass
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py b/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py
deleted file mode 100644
index 0c1952a16a..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestGoCardlessMandate(unittest.TestCase):
- pass
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py
deleted file mode 100644
index 65be5993ff..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-
-import hashlib
-import hmac
-import json
-
-import frappe
-
-
-@frappe.whitelist(allow_guest=True)
-def webhooks():
- r = frappe.request
- if not r:
- return
-
- if not authenticate_signature(r):
- raise frappe.AuthenticationError
-
- gocardless_events = json.loads(r.get_data()) or []
- for event in gocardless_events["events"]:
- set_status(event)
-
- return 200
-
-
-def set_status(event):
- resource_type = event.get("resource_type", {})
-
- if resource_type == "mandates":
- set_mandate_status(event)
-
-
-def set_mandate_status(event):
- mandates = []
- if isinstance(event["links"], (list,)):
- for link in event["links"]:
- mandates.append(link["mandate"])
- else:
- mandates.append(event["links"]["mandate"])
-
- if (
- event["action"] == "pending_customer_approval"
- or event["action"] == "pending_submission"
- or event["action"] == "submitted"
- or event["action"] == "active"
- ):
- disabled = 0
- else:
- disabled = 1
-
- for mandate in mandates:
- frappe.db.set_value("GoCardless Mandate", mandate, "disabled", disabled)
-
-
-def authenticate_signature(r):
- """Returns True if the received signature matches the generated signature"""
- received_signature = frappe.get_request_header("Webhook-Signature")
-
- if not received_signature:
- return False
-
- for key in get_webhook_keys():
- computed_signature = hmac.new(key.encode("utf-8"), r.get_data(), hashlib.sha256).hexdigest()
- if hmac.compare_digest(str(received_signature), computed_signature):
- return True
-
- return False
-
-
-def get_webhook_keys():
- def _get_webhook_keys():
- webhook_keys = [
- d.webhooks_secret
- for d in frappe.get_all(
- "GoCardless Settings",
- fields=["webhooks_secret"],
- )
- if d.webhooks_secret
- ]
-
- return webhook_keys
-
- return frappe.cache().get_value("gocardless_webhooks_secret", _get_webhook_keys)
-
-
-def clear_cache():
- frappe.cache().delete_value("gocardless_webhooks_secret")
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js
deleted file mode 100644
index 241129719b..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('GoCardless Settings', {
- refresh: function(frm) {
- erpnext.utils.check_payments_app();
- }
-});
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json
deleted file mode 100644
index cca36536ac..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json
+++ /dev/null
@@ -1,211 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:gateway_name",
- "beta": 0,
- "creation": "2018-02-06 16:11:10.028249",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gateway_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Gateway Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "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,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "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,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "access_token",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Access Token",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "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,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "webhooks_secret",
- "fieldtype": "Data",
- "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": "Webhooks Secret",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "use_sandbox",
- "fieldtype": "Check",
- "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": "Use Sandbox",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2022-02-12 14:18:47.209114",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "GoCardless Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
deleted file mode 100644
index 4a29a6a21d..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-
-from urllib.parse import urlencode
-
-import frappe
-import gocardless_pro
-from frappe import _
-from frappe.integrations.utils import create_request_log
-from frappe.model.document import Document
-from frappe.utils import call_hook_method, cint, flt, get_url
-
-from erpnext.utilities import payment_app_import_guard
-
-
-class GoCardlessSettings(Document):
- supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
-
- def validate(self):
- self.initialize_client()
-
- def initialize_client(self):
- self.environment = self.get_environment()
- try:
- self.client = gocardless_pro.Client(
- access_token=self.access_token, environment=self.environment
- )
- return self.client
- except Exception as e:
- frappe.throw(e)
-
- def on_update(self):
- with payment_app_import_guard():
- from payments.utils import create_payment_gateway
-
- create_payment_gateway(
- "GoCardless-" + self.gateway_name, settings="GoCardLess Settings", controller=self.gateway_name
- )
- call_hook_method("payment_gateway_enabled", gateway="GoCardless-" + self.gateway_name)
-
- def on_payment_request_submission(self, data):
- if data.reference_doctype != "Fees":
- customer_data = frappe.db.get_value(
- data.reference_doctype, data.reference_name, ["company", "customer_name"], as_dict=1
- )
-
- data = {
- "amount": flt(data.grand_total, data.precision("grand_total")),
- "title": customer_data.company.encode("utf-8"),
- "description": data.subject.encode("utf-8"),
- "reference_doctype": data.doctype,
- "reference_docname": data.name,
- "payer_email": data.email_to or frappe.session.user,
- "payer_name": customer_data.customer_name,
- "order_id": data.name,
- "currency": data.currency,
- }
-
- valid_mandate = self.check_mandate_validity(data)
- if valid_mandate is not None:
- data.update(valid_mandate)
-
- self.create_payment_request(data)
- return False
- else:
- return True
-
- def check_mandate_validity(self, data):
-
- if frappe.db.exists("GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0)):
- registered_mandate = frappe.db.get_value(
- "GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0), "mandate"
- )
- self.initialize_client()
- mandate = self.client.mandates.get(registered_mandate)
-
- if (
- mandate.status == "pending_customer_approval"
- or mandate.status == "pending_submission"
- or mandate.status == "submitted"
- or mandate.status == "active"
- ):
- return {"mandate": registered_mandate}
- else:
- return None
- else:
- return None
-
- def get_environment(self):
- if self.use_sandbox:
- return "sandbox"
- else:
- return "live"
-
- def validate_transaction_currency(self, currency):
- if currency not in self.supported_currencies:
- frappe.throw(
- _(
- "Please select another payment method. Go Cardless does not support transactions in currency '{0}'"
- ).format(currency)
- )
-
- def get_payment_url(self, **kwargs):
- return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
-
- def create_payment_request(self, data):
- self.data = frappe._dict(data)
-
- try:
- self.integration_request = create_request_log(self.data, "Host", "GoCardless")
- return self.create_charge_on_gocardless()
-
- except Exception:
- frappe.log_error("Gocardless payment reqeust failed")
- return {
- "redirect_to": frappe.redirect_to_message(
- _("Server Error"),
- _(
- "There seems to be an issue with the server's GoCardless configuration. Don't worry, in case of failure, the amount will get refunded to your account."
- ),
- ),
- "status": 401,
- }
-
- def create_charge_on_gocardless(self):
- redirect_to = self.data.get("redirect_to") or None
- redirect_message = self.data.get("redirect_message") or None
-
- reference_doc = frappe.get_doc(
- self.data.get("reference_doctype"), self.data.get("reference_docname")
- )
- self.initialize_client()
-
- try:
- payment = self.client.payments.create(
- params={
- "amount": cint(reference_doc.grand_total * 100),
- "currency": reference_doc.currency,
- "links": {"mandate": self.data.get("mandate")},
- "metadata": {
- "reference_doctype": reference_doc.doctype,
- "reference_document": reference_doc.name,
- },
- },
- headers={
- "Idempotency-Key": self.data.get("reference_docname"),
- },
- )
-
- if (
- payment.status == "pending_submission"
- or payment.status == "pending_customer_approval"
- or payment.status == "submitted"
- ):
- self.integration_request.db_set("status", "Authorized", update_modified=False)
- self.flags.status_changed_to = "Completed"
- self.integration_request.db_set("output", payment.status, update_modified=False)
-
- elif payment.status == "confirmed" or payment.status == "paid_out":
- self.integration_request.db_set("status", "Completed", update_modified=False)
- self.flags.status_changed_to = "Completed"
- self.integration_request.db_set("output", payment.status, update_modified=False)
-
- elif (
- payment.status == "cancelled"
- or payment.status == "customer_approval_denied"
- or payment.status == "charged_back"
- ):
- self.integration_request.db_set("status", "Cancelled", update_modified=False)
- frappe.log_error("Gocardless payment cancelled")
- self.integration_request.db_set("error", payment.status, update_modified=False)
- else:
- self.integration_request.db_set("status", "Failed", update_modified=False)
- frappe.log_error("Gocardless payment failed")
- self.integration_request.db_set("error", payment.status, update_modified=False)
-
- except Exception as e:
- frappe.log_error("GoCardless Payment Error")
-
- if self.flags.status_changed_to == "Completed":
- status = "Completed"
- if "reference_doctype" in self.data and "reference_docname" in self.data:
- custom_redirect_to = None
- try:
- custom_redirect_to = frappe.get_doc(
- self.data.get("reference_doctype"), self.data.get("reference_docname")
- ).run_method("on_payment_authorized", self.flags.status_changed_to)
- except Exception:
- frappe.log_error("Gocardless redirect failed")
-
- if custom_redirect_to:
- redirect_to = custom_redirect_to
-
- redirect_url = redirect_to
- else:
- status = "Error"
- redirect_url = "payment-failed"
-
- if redirect_message:
- redirect_url += "&" + urlencode({"redirect_message": redirect_message})
-
- redirect_url = get_url(redirect_url)
-
- return {"redirect_to": redirect_url, "status": status}
-
-
-def get_gateway_controller(doc):
- payment_request = frappe.get_doc("Payment Request", doc)
- gateway_controller = frappe.db.get_value(
- "Payment Gateway", payment_request.payment_gateway, "gateway_controller"
- )
- return gateway_controller
-
-
-def gocardless_initialization(doc):
- gateway_controller = get_gateway_controller(doc)
- settings = frappe.get_doc("GoCardless Settings", gateway_controller)
- client = settings.initialize_client()
- return client
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py
deleted file mode 100644
index 379afe51dd..0000000000
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestGoCardlessSettings(unittest.TestCase):
- pass
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html
deleted file mode 100644
index b74a7187f0..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-{% if not jQuery.isEmptyObject(data) %}
-
{{ __("Balance Details") }}
-
-
-
- {{ __("Account Type") }}
- {{ __("Current Balance") }}
- {{ __("Available Balance") }}
- {{ __("Reserved Balance") }}
- {{ __("Uncleared Balance") }}
-
-
-
- {% for(const [key, value] of Object.entries(data)) { %}
-
- {%= key %}
- {%= value["current_balance"] %}
- {%= value["available_balance"] %}
- {%= value["reserved_balance"] %}
- {%= value["uncleared_balance"] %}
-
- {% } %}
-
-
-{% else %}
-
Account Balance Information Not Available.
-{% endif %}
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
deleted file mode 100644
index a577e7fa69..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
+++ /dev/null
@@ -1,149 +0,0 @@
-import base64
-import datetime
-
-import requests
-from requests.auth import HTTPBasicAuth
-
-
-class MpesaConnector:
- def __init__(
- self,
- env="sandbox",
- app_key=None,
- app_secret=None,
- sandbox_url="https://sandbox.safaricom.co.ke",
- live_url="https://api.safaricom.co.ke",
- ):
- """Setup configuration for Mpesa connector and generate new access token."""
- self.env = env
- self.app_key = app_key
- self.app_secret = app_secret
- if env == "sandbox":
- self.base_url = sandbox_url
- else:
- self.base_url = live_url
- self.authenticate()
-
- def authenticate(self):
- """
- This method is used to fetch the access token required by Mpesa.
-
- Returns:
- access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa.
- """
- authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials"
- authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri)
- r = requests.get(authenticate_url, auth=HTTPBasicAuth(self.app_key, self.app_secret))
- self.authentication_token = r.json()["access_token"]
- return r.json()["access_token"]
-
- def get_balance(
- self,
- initiator=None,
- security_credential=None,
- party_a=None,
- identifier_type=None,
- remarks=None,
- queue_timeout_url=None,
- result_url=None,
- ):
- """
- This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
-
- Args:
- initiator (str): Username used to authenticate the transaction.
- security_credential (str): Generate from developer portal.
- command_id (str): AccountBalance.
- party_a (int): Till number being queried.
- identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code)
- remarks (str): Comments that are sent along with the transaction(maximum 100 characters).
- queue_timeout_url (str): The url that handles information of timed out transactions.
- result_url (str): The url that receives results from M-Pesa api call.
-
- Returns:
- OriginatorConverstionID (str): The unique request ID for tracking a transaction.
- ConversationID (str): The unique request ID returned by mpesa for each request made
- ResponseDescription (str): Response Description message
- """
-
- payload = {
- "Initiator": initiator,
- "SecurityCredential": security_credential,
- "CommandID": "AccountBalance",
- "PartyA": party_a,
- "IdentifierType": identifier_type,
- "Remarks": remarks,
- "QueueTimeOutURL": queue_timeout_url,
- "ResultURL": result_url,
- }
- headers = {
- "Authorization": "Bearer {0}".format(self.authentication_token),
- "Content-Type": "application/json",
- }
- saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query")
- r = requests.post(saf_url, headers=headers, json=payload)
- return r.json()
-
- def stk_push(
- self,
- business_shortcode=None,
- passcode=None,
- amount=None,
- callback_url=None,
- reference_code=None,
- phone_number=None,
- description=None,
- ):
- """
- This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
-
- Args:
- business_shortcode (int): The short code of the organization.
- passcode (str): Get from developer portal
- amount (int): The amount being transacted
- callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API.
- reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
- phone_number(int): The Mobile Number to receive the STK Pin Prompt.
- description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters
-
- Success Response:
- CustomerMessage(str): Messages that customers can understand.
- CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request.
- ResponseDescription(str): Describes Success or failure
- MerchantRequestID(str): This is a global unique Identifier for any submitted payment request.
- ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03
-
- Error Reponse:
- requestId(str): This is a unique requestID for the payment request
- errorCode(str): This is a predefined code that indicates the reason for request failure.
- errorMessage(str): This is a predefined code that indicates the reason for request failure.
- """
-
- time = (
- str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
- )
- password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time)
- encoded = base64.b64encode(bytes(password, encoding="utf8"))
- payload = {
- "BusinessShortCode": business_shortcode,
- "Password": encoded.decode("utf-8"),
- "Timestamp": time,
- "Amount": amount,
- "PartyA": int(phone_number),
- "PartyB": reference_code,
- "PhoneNumber": int(phone_number),
- "CallBackURL": callback_url,
- "AccountReference": reference_code,
- "TransactionDesc": description,
- "TransactionType": "CustomerPayBillOnline"
- if self.env == "sandbox"
- else "CustomerBuyGoodsOnline",
- }
- headers = {
- "Authorization": "Bearer {0}".format(self.authentication_token),
- "Content-Type": "application/json",
- }
-
- saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest")
- r = requests.post(saf_url, headers=headers, json=payload)
- return r.json()
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py
deleted file mode 100644
index c92edc5efa..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def create_custom_pos_fields():
- """Create custom fields corresponding to POS Settings and POS Invoice."""
- pos_field = {
- "POS Invoice": [
- {
- "fieldname": "request_for_payment",
- "label": "Request for Payment",
- "fieldtype": "Button",
- "hidden": 1,
- "insert_after": "contact_email",
- },
- {
- "fieldname": "mpesa_receipt_number",
- "label": "Mpesa Receipt Number",
- "fieldtype": "Data",
- "read_only": 1,
- "insert_after": "company",
- },
- ]
- }
- if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
- create_custom_fields(pos_field)
-
- record_dict = [
- {
- "doctype": "POS Field",
- "fieldname": "contact_mobile",
- "label": "Mobile No",
- "fieldtype": "Data",
- "options": "Phone",
- "parenttype": "POS Settings",
- "parent": "POS Settings",
- "parentfield": "invoice_fields",
- },
- {
- "doctype": "POS Field",
- "fieldname": "request_for_payment",
- "label": "Request for Payment",
- "fieldtype": "Button",
- "parenttype": "POS Settings",
- "parent": "POS Settings",
- "parentfield": "invoice_fields",
- },
- ]
- create_pos_settings(record_dict)
-
-
-def create_pos_settings(record_dict):
- for record in record_dict:
- if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}):
- continue
- frappe.get_doc(record).insert()
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js
deleted file mode 100644
index 447d720ca2..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Mpesa Settings', {
- onload_post_render: function(frm) {
- frm.events.setup_account_balance_html(frm);
- },
-
- refresh: function(frm) {
- erpnext.utils.check_payments_app();
-
- frappe.realtime.on("refresh_mpesa_dashboard", function(){
- frm.reload_doc();
- frm.events.setup_account_balance_html(frm);
- });
- },
-
- get_account_balance: function(frm) {
- if (!frm.doc.initiator_name && !frm.doc.security_credential) {
- frappe.throw(__("Please set the initiator name and the security credential"));
- }
- frappe.call({
- method: "get_account_balance_info",
- doc: frm.doc
- });
- },
-
- setup_account_balance_html: function(frm) {
- if (!frm.doc.account_balance) return;
- $("div").remove(".form-dashboard-section.custom");
- frm.dashboard.add_section(
- frappe.render_template('account_balance', {
- data: JSON.parse(frm.doc.account_balance)
- })
- );
- frm.dashboard.show();
- }
-
-});
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
deleted file mode 100644
index 8f3b4271c1..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
+++ /dev/null
@@ -1,152 +0,0 @@
-{
- "actions": [],
- "autoname": "field:payment_gateway_name",
- "creation": "2020-09-10 13:21:27.398088",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "payment_gateway_name",
- "consumer_key",
- "consumer_secret",
- "initiator_name",
- "till_number",
- "transaction_limit",
- "sandbox",
- "column_break_4",
- "business_shortcode",
- "online_passkey",
- "security_credential",
- "get_account_balance",
- "account_balance"
- ],
- "fields": [
- {
- "fieldname": "payment_gateway_name",
- "fieldtype": "Data",
- "label": "Payment Gateway Name",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Consumer Key",
- "reqd": 1
- },
- {
- "fieldname": "consumer_secret",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "Consumer Secret",
- "reqd": 1
- },
- {
- "fieldname": "till_number",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Till Number",
- "reqd": 1
- },
- {
- "default": "0",
- "fieldname": "sandbox",
- "fieldtype": "Check",
- "label": "Sandbox"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "online_passkey",
- "fieldtype": "Password",
- "label": " Online PassKey",
- "reqd": 1
- },
- {
- "fieldname": "initiator_name",
- "fieldtype": "Data",
- "label": "Initiator Name"
- },
- {
- "fieldname": "security_credential",
- "fieldtype": "Small Text",
- "label": "Security Credential"
- },
- {
- "fieldname": "account_balance",
- "fieldtype": "Long Text",
- "hidden": 1,
- "label": "Account Balance",
- "read_only": 1
- },
- {
- "fieldname": "get_account_balance",
- "fieldtype": "Button",
- "label": "Get Account Balance"
- },
- {
- "depends_on": "eval:(doc.sandbox==0)",
- "fieldname": "business_shortcode",
- "fieldtype": "Data",
- "label": "Business Shortcode",
- "mandatory_depends_on": "eval:(doc.sandbox==0)"
- },
- {
- "default": "150000",
- "fieldname": "transaction_limit",
- "fieldtype": "Float",
- "label": "Transaction Limit",
- "non_negative": 1
- }
- ],
- "links": [],
- "modified": "2021-03-02 17:35:14.084342",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "Mpesa Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
deleted file mode 100644
index a298e11eaf..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ /dev/null
@@ -1,354 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-
-from json import dumps, loads
-
-import frappe
-from frappe import _
-from frappe.integrations.utils import create_request_log
-from frappe.model.document import Document
-from frappe.utils import call_hook_method, fmt_money, get_request_site_address
-
-from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
-from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import (
- create_custom_pos_fields,
-)
-from erpnext.erpnext_integrations.utils import create_mode_of_payment
-from erpnext.utilities import payment_app_import_guard
-
-
-class MpesaSettings(Document):
- supported_currencies = ["KES"]
-
- def validate_transaction_currency(self, currency):
- if currency not in self.supported_currencies:
- frappe.throw(
- _(
- "Please select another payment method. Mpesa does not support transactions in currency '{0}'"
- ).format(currency)
- )
-
- def on_update(self):
- with payment_app_import_guard():
- from payments.utils import create_payment_gateway
-
- create_custom_pos_fields()
- create_payment_gateway(
- "Mpesa-" + self.payment_gateway_name,
- settings="Mpesa Settings",
- controller=self.payment_gateway_name,
- )
- call_hook_method(
- "payment_gateway_enabled", gateway="Mpesa-" + self.payment_gateway_name, payment_channel="Phone"
- )
-
- # required to fetch the bank account details from the payment gateway account
- frappe.db.commit()
- create_mode_of_payment("Mpesa-" + self.payment_gateway_name, payment_type="Phone")
-
- def request_for_payment(self, **kwargs):
- args = frappe._dict(kwargs)
- request_amounts = self.split_request_amount_according_to_transaction_limit(args)
-
- for i, amount in enumerate(request_amounts):
- args.request_amount = amount
- if frappe.flags.in_test:
- from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import (
- get_payment_request_response_payload,
- )
-
- response = frappe._dict(get_payment_request_response_payload(amount))
- else:
- response = frappe._dict(generate_stk_push(**args))
-
- self.handle_api_response("CheckoutRequestID", args, response)
-
- def split_request_amount_according_to_transaction_limit(self, args):
- request_amount = args.request_amount
- if request_amount > self.transaction_limit:
- # make multiple requests
- request_amounts = []
- requests_to_be_made = frappe.utils.ceil(
- request_amount / self.transaction_limit
- ) # 480/150 = ceil(3.2) = 4
- for i in range(requests_to_be_made):
- amount = self.transaction_limit
- if i == requests_to_be_made - 1:
- amount = request_amount - (
- self.transaction_limit * i
- ) # for 4th request, 480 - (150 * 3) = 30
- request_amounts.append(amount)
- else:
- request_amounts = [request_amount]
-
- return request_amounts
-
- @frappe.whitelist()
- def get_account_balance_info(self):
- payload = dict(
- reference_doctype="Mpesa Settings", reference_docname=self.name, doc_details=vars(self)
- )
-
- if frappe.flags.in_test:
- from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import (
- get_test_account_balance_response,
- )
-
- response = frappe._dict(get_test_account_balance_response())
- else:
- response = frappe._dict(get_account_balance(payload))
-
- self.handle_api_response("ConversationID", payload, response)
-
- def handle_api_response(self, global_id, request_dict, response):
- """Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
- # check error response
- if getattr(response, "requestId"):
- req_name = getattr(response, "requestId")
- error = response
- else:
- # global checkout id used as request name
- req_name = getattr(response, global_id)
- error = None
-
- if not frappe.db.exists("Integration Request", req_name):
- create_request_log(request_dict, "Host", "Mpesa", req_name, error)
-
- if error:
- frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
-
-
-def generate_stk_push(**kwargs):
- """Generate stk push by making a API call to the stk push API."""
- args = frappe._dict(kwargs)
- try:
- callback_url = (
- get_request_site_address(True)
- + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
- )
-
- mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
- env = "production" if not mpesa_settings.sandbox else "sandbox"
- # for sandbox, business shortcode is same as till number
- business_shortcode = (
- mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
- )
-
- connector = MpesaConnector(
- env=env,
- app_key=mpesa_settings.consumer_key,
- app_secret=mpesa_settings.get_password("consumer_secret"),
- )
-
- mobile_number = sanitize_mobile_number(args.sender)
-
- response = connector.stk_push(
- business_shortcode=business_shortcode,
- amount=args.request_amount,
- passcode=mpesa_settings.get_password("online_passkey"),
- callback_url=callback_url,
- reference_code=mpesa_settings.till_number,
- phone_number=mobile_number,
- description="POS Payment",
- )
-
- return response
-
- except Exception:
- frappe.log_error("Mpesa Express Transaction Error")
- frappe.throw(
- _("Issue detected with Mpesa configuration, check the error logs for more details"),
- title=_("Mpesa Express Error"),
- )
-
-
-def sanitize_mobile_number(number):
- """Add country code and strip leading zeroes from the phone number."""
- return "254" + str(number).lstrip("0")
-
-
-@frappe.whitelist(allow_guest=True)
-def verify_transaction(**kwargs):
- """Verify the transaction result received via callback from stk."""
- transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
-
- checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
- if not isinstance(checkout_id, str):
- frappe.throw(_("Invalid Checkout Request ID"))
-
- integration_request = frappe.get_doc("Integration Request", checkout_id)
- transaction_data = frappe._dict(loads(integration_request.data))
- total_paid = 0 # for multiple integration request made against a pos invoice
- success = False # for reporting successfull callback to point of sale ui
-
- if transaction_response["ResultCode"] == 0:
- if integration_request.reference_doctype and integration_request.reference_docname:
- try:
- item_response = transaction_response["CallbackMetadata"]["Item"]
- amount = fetch_param_value(item_response, "Amount", "Name")
- mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
- pr = frappe.get_doc(
- integration_request.reference_doctype, integration_request.reference_docname
- )
-
- mpesa_receipts, completed_payments = get_completed_integration_requests_info(
- integration_request.reference_doctype, integration_request.reference_docname, checkout_id
- )
-
- total_paid = amount + sum(completed_payments)
- mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt])
-
- if total_paid >= pr.grand_total:
- pr.run_method("on_payment_authorized", "Completed")
- success = True
-
- frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
- integration_request.handle_success(transaction_response)
- except Exception:
- integration_request.handle_failure(transaction_response)
- frappe.log_error("Mpesa: Failed to verify transaction")
-
- else:
- integration_request.handle_failure(transaction_response)
-
- frappe.publish_realtime(
- event="process_phone_payment",
- doctype="POS Invoice",
- docname=transaction_data.payment_reference,
- user=integration_request.owner,
- message={
- "amount": total_paid,
- "success": success,
- "failure_message": transaction_response["ResultDesc"]
- if transaction_response["ResultCode"] != 0
- else "",
- },
- )
-
-
-def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
- output_of_other_completed_requests = frappe.get_all(
- "Integration Request",
- filters={
- "name": ["!=", checkout_id],
- "reference_doctype": reference_doctype,
- "reference_docname": reference_docname,
- "status": "Completed",
- },
- pluck="output",
- )
-
- mpesa_receipts, completed_payments = [], []
-
- for out in output_of_other_completed_requests:
- out = frappe._dict(loads(out))
- item_response = out["CallbackMetadata"]["Item"]
- completed_amount = fetch_param_value(item_response, "Amount", "Name")
- completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
- completed_payments.append(completed_amount)
- mpesa_receipts.append(completed_mpesa_receipt)
-
- return mpesa_receipts, completed_payments
-
-
-def get_account_balance(request_payload):
- """Call account balance API to send the request to the Mpesa Servers."""
- try:
- mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname"))
- env = "production" if not mpesa_settings.sandbox else "sandbox"
- connector = MpesaConnector(
- env=env,
- app_key=mpesa_settings.consumer_key,
- app_secret=mpesa_settings.get_password("consumer_secret"),
- )
-
- callback_url = (
- get_request_site_address(True)
- + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
- )
-
- response = connector.get_balance(
- mpesa_settings.initiator_name,
- mpesa_settings.security_credential,
- mpesa_settings.till_number,
- 4,
- mpesa_settings.name,
- callback_url,
- callback_url,
- )
- return response
- except Exception:
- frappe.log_error("Mpesa: Failed to get account balance")
- frappe.throw(_("Please check your configuration and try again"), title=_("Error"))
-
-
-@frappe.whitelist(allow_guest=True)
-def process_balance_info(**kwargs):
- """Process and store account balance information received via callback from the account balance API call."""
- account_balance_response = frappe._dict(kwargs["Result"])
-
- conversation_id = getattr(account_balance_response, "ConversationID", "")
- if not isinstance(conversation_id, str):
- frappe.throw(_("Invalid Conversation ID"))
-
- request = frappe.get_doc("Integration Request", conversation_id)
-
- if request.status == "Completed":
- return
-
- transaction_data = frappe._dict(loads(request.data))
-
- if account_balance_response["ResultCode"] == 0:
- try:
- result_params = account_balance_response["ResultParameters"]["ResultParameter"]
-
- balance_info = fetch_param_value(result_params, "AccountBalance", "Key")
- balance_info = format_string_to_json(balance_info)
-
- ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname)
- ref_doc.db_set("account_balance", balance_info)
-
- request.handle_success(account_balance_response)
- frappe.publish_realtime(
- "refresh_mpesa_dashboard",
- doctype="Mpesa Settings",
- docname=transaction_data.reference_docname,
- user=transaction_data.owner,
- )
- except Exception:
- request.handle_failure(account_balance_response)
- frappe.log_error(
- title="Mpesa Account Balance Processing Error", message=account_balance_response
- )
- else:
- request.handle_failure(account_balance_response)
-
-
-def format_string_to_json(balance_info):
- """
- Format string to json.
-
- e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00'''
- => {'Working Account': {'current_balance': '481000.00',
- 'available_balance': '481000.00',
- 'reserved_balance': '0.00',
- 'uncleared_balance': '0.00'}}
- """
- balance_dict = frappe._dict()
- for account_info in balance_info.split("&"):
- account_info = account_info.split("|")
- balance_dict[account_info[0]] = dict(
- current_balance=fmt_money(account_info[2], currency="KES"),
- available_balance=fmt_money(account_info[3], currency="KES"),
- reserved_balance=fmt_money(account_info[4], currency="KES"),
- uncleared_balance=fmt_money(account_info[5], currency="KES"),
- )
- return dumps(balance_dict)
-
-
-def fetch_param_value(response, key, key_field):
- """Fetch the specified key from list of dictionary. Key is identified via the key field."""
- for param in response:
- if param[key_field] == key:
- return param["Value"]
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
deleted file mode 100644
index b52662421d..0000000000
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
+++ /dev/null
@@ -1,361 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-from json import dumps
-
-import frappe
-
-from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
-from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import (
- process_balance_info,
- verify_transaction,
-)
-from erpnext.erpnext_integrations.utils import create_mode_of_payment
-
-
-class TestMpesaSettings(unittest.TestCase):
- def setUp(self):
- # create payment gateway in setup
- create_mpesa_settings(payment_gateway_name="_Test")
- create_mpesa_settings(payment_gateway_name="_Account Balance")
- create_mpesa_settings(payment_gateway_name="Payment")
-
- def tearDown(self):
- frappe.db.sql("delete from `tabMpesa Settings`")
- frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
-
- def test_creation_of_payment_gateway(self):
- mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone")
- self.assertTrue(frappe.db.exists("Payment Gateway Account", {"payment_gateway": "Mpesa-_Test"}))
- self.assertTrue(mode_of_payment.name)
- self.assertEqual(mode_of_payment.type, "Phone")
-
- def test_processing_of_account_balance(self):
- mpesa_doc = create_mpesa_settings(payment_gateway_name="_Account Balance")
- mpesa_doc.get_account_balance_info()
-
- callback_response = get_account_balance_callback_payload()
- process_balance_info(**callback_response)
- integration_request = frappe.get_doc("Integration Request", "AG_20200927_00007cdb1f9fb6494315")
-
- # test integration request creation and successful update of the status on receiving callback response
- self.assertTrue(integration_request)
- self.assertEqual(integration_request.status, "Completed")
-
- # test formatting of account balance received as string to json with appropriate currency symbol
- mpesa_doc.reload()
- self.assertEqual(
- mpesa_doc.account_balance,
- dumps(
- {
- "Working Account": {
- "current_balance": "Sh 481,000.00",
- "available_balance": "Sh 481,000.00",
- "reserved_balance": "Sh 0.00",
- "uncleared_balance": "Sh 0.00",
- }
- }
- ),
- )
-
- integration_request.delete()
-
- def test_processing_of_callback_payload(self):
- mpesa_account = frappe.db.get_value(
- "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account"
- )
- frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
- frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
-
- pos_invoice = create_pos_invoice(do_not_submit=1)
- pos_invoice.append(
- "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 500}
- )
- pos_invoice.contact_mobile = "093456543894"
- pos_invoice.currency = "KES"
- pos_invoice.save()
-
- pr = pos_invoice.create_payment_request()
- # test payment request creation
- self.assertEqual(pr.payment_gateway, "Mpesa-Payment")
-
- # submitting payment request creates integration requests with random id
- integration_req_ids = frappe.get_all(
- "Integration Request",
- filters={
- "reference_doctype": pr.doctype,
- "reference_docname": pr.name,
- },
- pluck="name",
- )
-
- callback_response = get_payment_callback_payload(
- Amount=500, CheckoutRequestID=integration_req_ids[0]
- )
- verify_transaction(**callback_response)
- # test creation of integration request
- integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
-
- # test integration request creation and successful update of the status on receiving callback response
- self.assertTrue(integration_request)
- self.assertEqual(integration_request.status, "Completed")
-
- pos_invoice.reload()
- integration_request.reload()
- self.assertEqual(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
- self.assertEqual(integration_request.status, "Completed")
-
- frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
- integration_request.delete()
- pr.reload()
- pr.cancel()
- pr.delete()
- pos_invoice.delete()
-
- def test_processing_of_multiple_callback_payload(self):
- mpesa_account = frappe.db.get_value(
- "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account"
- )
- frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
- frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
- frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
-
- pos_invoice = create_pos_invoice(do_not_submit=1)
- pos_invoice.append(
- "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000}
- )
- pos_invoice.contact_mobile = "093456543894"
- pos_invoice.currency = "KES"
- pos_invoice.save()
-
- pr = pos_invoice.create_payment_request()
- # test payment request creation
- self.assertEqual(pr.payment_gateway, "Mpesa-Payment")
-
- # submitting payment request creates integration requests with random id
- integration_req_ids = frappe.get_all(
- "Integration Request",
- filters={
- "reference_doctype": pr.doctype,
- "reference_docname": pr.name,
- },
- pluck="name",
- )
-
- # create random receipt nos and send it as response to callback handler
- mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
-
- integration_requests = []
- for i in range(len(integration_req_ids)):
- callback_response = get_payment_callback_payload(
- Amount=500,
- CheckoutRequestID=integration_req_ids[i],
- MpesaReceiptNumber=mpesa_receipt_numbers[i],
- )
- # handle response manually
- verify_transaction(**callback_response)
- # test completion of integration request
- integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
- self.assertEqual(integration_request.status, "Completed")
- integration_requests.append(integration_request)
-
- # check receipt number once all the integration requests are completed
- pos_invoice.reload()
- self.assertEqual(pos_invoice.mpesa_receipt_number, ", ".join(mpesa_receipt_numbers))
-
- frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
- [d.delete() for d in integration_requests]
- pr.reload()
- pr.cancel()
- pr.delete()
- pos_invoice.delete()
-
- def test_processing_of_only_one_succes_callback_payload(self):
- mpesa_account = frappe.db.get_value(
- "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account"
- )
- frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
- frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
- frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
-
- pos_invoice = create_pos_invoice(do_not_submit=1)
- pos_invoice.append(
- "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000}
- )
- pos_invoice.contact_mobile = "093456543894"
- pos_invoice.currency = "KES"
- pos_invoice.save()
-
- pr = pos_invoice.create_payment_request()
- # test payment request creation
- self.assertEqual(pr.payment_gateway, "Mpesa-Payment")
-
- # submitting payment request creates integration requests with random id
- integration_req_ids = frappe.get_all(
- "Integration Request",
- filters={
- "reference_doctype": pr.doctype,
- "reference_docname": pr.name,
- },
- pluck="name",
- )
-
- # create random receipt nos and send it as response to callback handler
- mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
-
- callback_response = get_payment_callback_payload(
- Amount=500,
- CheckoutRequestID=integration_req_ids[0],
- MpesaReceiptNumber=mpesa_receipt_numbers[0],
- )
- # handle response manually
- verify_transaction(**callback_response)
- # test completion of integration request
- integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
- self.assertEqual(integration_request.status, "Completed")
-
- # now one request is completed
- # second integration request fails
- # now retrying payment request should make only one integration request again
- pr = pos_invoice.create_payment_request()
- new_integration_req_ids = frappe.get_all(
- "Integration Request",
- filters={
- "reference_doctype": pr.doctype,
- "reference_docname": pr.name,
- "name": ["not in", integration_req_ids],
- },
- pluck="name",
- )
-
- self.assertEqual(len(new_integration_req_ids), 1)
-
- frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
- frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
- pr.reload()
- pr.cancel()
- pr.delete()
- pos_invoice.delete()
-
-
-def create_mpesa_settings(payment_gateway_name="Express"):
- if frappe.db.exists("Mpesa Settings", payment_gateway_name):
- return frappe.get_doc("Mpesa Settings", payment_gateway_name)
-
- doc = frappe.get_doc(
- dict( # nosec
- doctype="Mpesa Settings",
- sandbox=1,
- payment_gateway_name=payment_gateway_name,
- consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
- consumer_secret="VI1oS3oBGPJfh3JyvLHw",
- online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd",
- till_number="174379",
- )
- )
-
- doc.insert(ignore_permissions=True)
- return doc
-
-
-def get_test_account_balance_response():
- """Response received after calling the account balance API."""
- return {
- "ResultType": 0,
- "ResultCode": 0,
- "ResultDesc": "The service request has been accepted successfully.",
- "OriginatorConversationID": "10816-694520-2",
- "ConversationID": "AG_20200927_00007cdb1f9fb6494315",
- "TransactionID": "LGR0000000",
- "ResultParameters": {
- "ResultParameter": [
- {"Key": "ReceiptNo", "Value": "LGR919G2AV"},
- {"Key": "Conversation ID", "Value": "AG_20170727_00004492b1b6d0078fbe"},
- {"Key": "FinalisedTime", "Value": 20170727101415},
- {"Key": "Amount", "Value": 10},
- {"Key": "TransactionStatus", "Value": "Completed"},
- {"Key": "ReasonType", "Value": "Salary Payment via API"},
- {"Key": "TransactionReason"},
- {"Key": "DebitPartyCharges", "Value": "Fee For B2C Payment|KES|33.00"},
- {"Key": "DebitAccountType", "Value": "Utility Account"},
- {"Key": "InitiatedTime", "Value": 20170727101415},
- {"Key": "Originator Conversation ID", "Value": "19455-773836-1"},
- {"Key": "CreditPartyName", "Value": "254708374149 - John Doe"},
- {"Key": "DebitPartyName", "Value": "600134 - Safaricom157"},
- ]
- },
- "ReferenceData": {"ReferenceItem": {"Key": "Occasion", "Value": "aaaa"}},
- }
-
-
-def get_payment_request_response_payload(Amount=500):
- """Response received after successfully calling the stk push process request API."""
-
- CheckoutRequestID = frappe.utils.random_string(10)
-
- return {
- "MerchantRequestID": "8071-27184008-1",
- "CheckoutRequestID": CheckoutRequestID,
- "ResultCode": 0,
- "ResultDesc": "The service request is processed successfully.",
- "CallbackMetadata": {
- "Item": [
- {"Name": "Amount", "Value": Amount},
- {"Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R"},
- {"Name": "TransactionDate", "Value": 20201006113336},
- {"Name": "PhoneNumber", "Value": 254723575670},
- ]
- },
- }
-
-
-def get_payment_callback_payload(
- Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"
-):
- """Response received from the server as callback after calling the stkpush process request API."""
- return {
- "Body": {
- "stkCallback": {
- "MerchantRequestID": "19465-780693-1",
- "CheckoutRequestID": CheckoutRequestID,
- "ResultCode": 0,
- "ResultDesc": "The service request is processed successfully.",
- "CallbackMetadata": {
- "Item": [
- {"Name": "Amount", "Value": Amount},
- {"Name": "MpesaReceiptNumber", "Value": MpesaReceiptNumber},
- {"Name": "Balance"},
- {"Name": "TransactionDate", "Value": 20170727154800},
- {"Name": "PhoneNumber", "Value": 254721566839},
- ]
- },
- }
- }
- }
-
-
-def get_account_balance_callback_payload():
- """Response received from the server as callback after calling the account balance API."""
- return {
- "Result": {
- "ResultType": 0,
- "ResultCode": 0,
- "ResultDesc": "The service request is processed successfully.",
- "OriginatorConversationID": "16470-170099139-1",
- "ConversationID": "AG_20200927_00007cdb1f9fb6494315",
- "TransactionID": "OIR0000000",
- "ResultParameters": {
- "ResultParameter": [
- {"Key": "AccountBalance", "Value": "Working Account|KES|481000.00|481000.00|0.00|0.00"},
- {"Key": "BOCompletedTime", "Value": 20200927234123},
- ]
- },
- "ReferenceData": {
- "ReferenceItem": {
- "Key": "QueueTimeoutURL",
- "Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit",
- }
- },
- }
- }
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 11d5f6a9c4..eb99345991 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
-from frappe.utils import add_months, formatdate, getdate, today
+from frappe.utils import add_months, formatdate, getdate, sbool, today
from plaid.errors import ItemError
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
@@ -237,8 +237,6 @@ def new_bank_transaction(transaction):
deposit = abs(amount)
withdrawal = 0.0
- status = "Pending" if transaction["pending"] == True else "Settled"
-
tags = []
if transaction["category"]:
try:
@@ -247,13 +245,14 @@ def new_bank_transaction(transaction):
except KeyError:
pass
- if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
+ if not frappe.db.exists(
+ "Bank Transaction", dict(transaction_id=transaction["transaction_id"])
+ ) and not sbool(transaction["pending"]):
try:
new_transaction = frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": getdate(transaction["date"]),
- "status": status,
"bank_account": bank_account,
"deposit": deposit,
"withdrawal": withdrawal,
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index 86e1b31eba..67168536e7 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
@@ -43,40 +43,6 @@ class TestPlaidSettings(unittest.TestCase):
add_account_subtype("loan")
self.assertEqual(frappe.get_doc("Bank Account Subtype", "loan").name, "loan")
- def test_default_bank_account(self):
- if not frappe.db.exists("Bank", "Citi"):
- frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert()
-
- bank_accounts = {
- "account": {
- "subtype": "checking",
- "mask": "0000",
- "type": "depository",
- "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK",
- "name": "Plaid Checking",
- },
- "account_id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK",
- "link_session_id": "db673d75-61aa-442a-864f-9b3f174f3725",
- "accounts": [
- {
- "type": "depository",
- "subtype": "checking",
- "mask": "0000",
- "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK",
- "name": "Plaid Checking",
- }
- ],
- "institution": {"institution_id": "ins_6", "name": "Citi"},
- }
-
- bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
- company = frappe.db.get_single_value("Global Defaults", "default_company")
- frappe.db.set_value("Company", company, "default_bank_account", None)
-
- self.assertRaises(
- frappe.ValidationError, add_bank_accounts, response=bank_accounts, bank=bank, company=company
- )
-
def test_new_transaction(self):
if not frappe.db.exists("Bank", "Citi"):
frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert()
diff --git a/erpnext/erpnext_integrations/stripe_integration.py b/erpnext/erpnext_integrations/stripe_integration.py
deleted file mode 100644
index 634e5c2e89..0000000000
--- a/erpnext/erpnext_integrations/stripe_integration.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-import frappe
-from frappe import _
-from frappe.integrations.utils import create_request_log
-
-from erpnext.utilities import payment_app_import_guard
-
-
-def create_stripe_subscription(gateway_controller, data):
- with payment_app_import_guard():
- import stripe
-
- stripe_settings = frappe.get_doc("Stripe Settings", gateway_controller)
- stripe_settings.data = frappe._dict(data)
-
- stripe.api_key = stripe_settings.get_password(fieldname="secret_key", raise_exception=False)
- stripe.default_http_client = stripe.http_client.RequestsClient()
-
- try:
- stripe_settings.integration_request = create_request_log(stripe_settings.data, "Host", "Stripe")
- stripe_settings.payment_plans = frappe.get_doc(
- "Payment Request", stripe_settings.data.reference_docname
- ).subscription_plans
- return create_subscription_on_stripe(stripe_settings)
-
- except Exception:
- stripe_settings.log_error("Unable to create Stripe subscription")
- return {
- "redirect_to": frappe.redirect_to_message(
- _("Server Error"),
- _(
- "It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account."
- ),
- ),
- "status": 401,
- }
-
-
-def create_subscription_on_stripe(stripe_settings):
- with payment_app_import_guard():
- import stripe
-
- items = []
- for payment_plan in stripe_settings.payment_plans:
- plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "product_price_id")
- items.append({"price": plan, "quantity": payment_plan.qty})
-
- try:
- customer = stripe.Customer.create(
- source=stripe_settings.data.stripe_token_id,
- description=stripe_settings.data.payer_name,
- email=stripe_settings.data.payer_email,
- )
-
- subscription = stripe.Subscription.create(customer=customer, items=items)
-
- if subscription.status == "active":
- stripe_settings.integration_request.db_set("status", "Completed", update_modified=False)
- stripe_settings.flags.status_changed_to = "Completed"
-
- else:
- stripe_settings.integration_request.db_set("status", "Failed", update_modified=False)
- frappe.log_error(f"Stripe Subscription ID {subscription.id}: Payment failed")
- except Exception:
- stripe_settings.integration_request.db_set("status", "Failed", update_modified=False)
- stripe_settings.log_error("Unable to create Stripe subscription")
-
- return stripe_settings.finalize_request()
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index 981486eb30..8984f1bee7 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -6,8 +6,6 @@ from urllib.parse import urlparse
import frappe
from frappe import _
-from erpnext import get_default_company
-
def validate_webhooks_request(doctype, hmac_key, secret_key="secret"):
def innerfn(fn):
@@ -47,35 +45,6 @@ def get_webhook_address(connector_name, method, exclude_uri=False, force_https=F
return server_url
-def create_mode_of_payment(gateway, payment_type="General"):
- payment_gateway_account = frappe.db.get_value(
- "Payment Gateway Account", {"payment_gateway": gateway}, ["payment_account"]
- )
-
- mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
- if not mode_of_payment and payment_gateway_account:
- mode_of_payment = frappe.get_doc(
- {
- "doctype": "Mode of Payment",
- "mode_of_payment": gateway,
- "enabled": 1,
- "type": payment_type,
- "accounts": [
- {
- "doctype": "Mode of Payment Account",
- "company": get_default_company(),
- "default_account": payment_gateway_account,
- }
- ],
- }
- )
- mode_of_payment.insert(ignore_permissions=True)
-
- return mode_of_payment
- elif mode_of_payment:
- return frappe.get_doc("Mode of Payment", mode_of_payment)
-
-
def get_tracking_url(carrier, tracking_number):
# Return the formatted Tracking URL.
tracking_url = ""
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 2155699a4c..5483a10b57 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -52,11 +52,7 @@ leaderboards = "erpnext.startup.leaderboard.get_leaderboards"
filters_config = "erpnext.startup.filters.get_filters_config"
additional_print_settings = "erpnext.controllers.print_settings.get_print_settings"
-on_session_creation = [
- "erpnext.portal.utils.create_customer_or_supplier",
- "erpnext.e_commerce.shopping_cart.utils.set_cart_count",
-]
-on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count"
+on_session_creation = "erpnext.portal.utils.create_customer_or_supplier"
treeviews = [
"Account",
@@ -90,15 +86,11 @@ jinja = {
}
# website
-update_website_context = [
- "erpnext.e_commerce.shopping_cart.utils.update_website_context",
-]
-my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = ["Task", "Work Order", "Sales Order", "Holiday List", "ToDo"]
-website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"]
+website_generators = ["BOM", "Sales Partner"]
website_context = {
"favicon": "/assets/erpnext/images/erpnext-favicon.svg",
@@ -349,9 +341,6 @@ doc_events = {
"Event": {
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
},
- "Sales Taxes and Charges Template": {
- "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
- },
"Sales Invoice": {
"on_submit": [
"erpnext.regional.create_transaction_log",
@@ -519,6 +508,7 @@ accounting_dimension_doctypes = [
"Sales Invoice Item",
"Purchase Invoice Item",
"Purchase Order Item",
+ "Sales Order Item",
"Journal Entry Account",
"Material Request Item",
"Delivery Note Item",
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
index 32f1c365ad..0135a4f971 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
@@ -107,7 +107,7 @@ def validate_against_blanket_order(order_doc):
allowance = flt(
frappe.db.get_single_value(
"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
- "over_order_allowance",
+ "blanket_order_allowance",
)
)
for bo_name, item_data in order_data.items():
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index 58f3c95059..e9fc25b5bc 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -63,7 +63,7 @@ class TestBlanketOrder(FrappeTestCase):
po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
- def test_over_order_allowance(self):
+ def test_blanket_order_allowance(self):
# Sales Order
bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
@@ -74,7 +74,7 @@ class TestBlanketOrder(FrappeTestCase):
so.items[0].qty = 110
self.assertRaises(frappe.ValidationError, so.submit)
- frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
+ frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 10)
so.submit()
# Purchase Order
@@ -87,7 +87,7 @@ class TestBlanketOrder(FrappeTestCase):
po.items[0].qty = 110
self.assertRaises(frappe.ValidationError, po.submit)
- frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
+ frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10)
po.submit()
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 023166849d..229f8853ff 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1196,12 +1196,12 @@ def get_children(parent=None, is_root=False, **filters):
def add_additional_cost(stock_entry, work_order):
# Add non stock items cost in the additional cost
stock_entry.additional_costs = []
- expenses_included_in_valuation = frappe.get_cached_value(
- "Company", work_order.company, "expenses_included_in_valuation"
+ default_expense_account = frappe.get_cached_value(
+ "Company", work_order.company, "default_expense_account"
)
- add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation)
- add_operations_cost(stock_entry, work_order, expenses_included_in_valuation)
+ add_non_stock_items_cost(stock_entry, work_order, default_expense_account)
+ add_operations_cost(stock_entry, work_order, default_expense_account)
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
index f4877fdca0..9e32085351 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
@@ -10,8 +10,8 @@ frappe.views.calendar["Job Card"] = {
},
gantt: {
field_map: {
- "start": "started_time",
- "end": "started_time",
+ "start": "expected_start_date",
+ "end": "expected_end_date",
"id": "name",
"title": "subject",
"color": "color",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js
index 5d883bf9fa..99fca9570f 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_list.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js
@@ -1,6 +1,6 @@
frappe.listview_settings['Job Card'] = {
has_indicator_for_draft: true,
-
+ add_fields: ["expected_start_date", "expected_end_date"],
get_indicator: function(doc) {
const status_colors = {
"Work In Progress": "orange",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index deef020220..ddd9375211 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -8,7 +8,6 @@ import json
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
-from frappe.query_builder import Case
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
add_days,
@@ -1618,21 +1617,13 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Material Request Plan Item")
- completed_production_plans = get_completed_production_plans()
+ non_completed_production_plans = get_non_completed_production_plans()
- case = Case()
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
- .select(
- Sum(
- child.quantity
- * IfNull(
- case.when(child.material_request_type == "Purchase", child.conversion_factor).else_(1.0), 1.0
- )
- )
- )
+ .select(Sum(child.required_bom_qty))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
@@ -1641,8 +1632,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
)
)
- if completed_production_plans:
- query = query.where(table.name.notin(completed_production_plans))
+ if non_completed_production_plans:
+ query = query.where(table.name.isin(non_completed_production_plans))
query = query.run()
@@ -1653,7 +1644,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
reserved_qty_for_production = flt(
get_reserved_qty_for_production(
- item_code, warehouse, completed_production_plans, check_production_plan=True
+ item_code, warehouse, non_completed_production_plans, check_production_plan=True
)
)
@@ -1663,7 +1654,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
return reserved_qty_for_production_plan - reserved_qty_for_production
-def get_completed_production_plans():
+def get_non_completed_production_plans():
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Production Plan Item")
@@ -1675,7 +1666,7 @@ def get_completed_production_plans():
.where(
(table.docstatus == 1)
& (table.status.notin(["Completed", "Closed"]))
- & (child.ordered_qty >= child.planned_qty)
+ & (child.planned_qty > child.ordered_qty)
)
).run(as_dict=True)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 4ff9d29e0b..6ab9232788 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -6,8 +6,8 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.production_plan import (
- get_completed_production_plans,
get_items_for_material_requests,
+ get_non_completed_production_plans,
get_sales_orders,
get_warehouse_list,
)
@@ -1143,9 +1143,9 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(after_qty, before_qty)
- completed_plans = get_completed_production_plans()
+ completed_plans = get_non_completed_production_plans()
for plan in plans:
- self.assertTrue(plan in completed_plans)
+ self.assertFalse(plan in completed_plans)
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
from erpnext.stock.utils import get_or_make_bin
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 3dc33ac578..f9fddcbb5e 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1515,7 +1515,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
def get_reserved_qty_for_production(
item_code: str,
warehouse: str,
- completed_production_plans: list = None,
+ non_completed_production_plans: list = None,
check_production_plan: bool = False,
) -> float:
"""Get total reserved quantity for any item in specified warehouse"""
@@ -1538,19 +1538,22 @@ def get_reserved_qty_for_production(
& (wo_item.parent == wo.name)
& (wo.docstatus == 1)
& (wo_item.source_warehouse == warehouse)
- & (wo.status.notin(["Stopped", "Completed", "Closed"]))
- & (
- (wo_item.required_qty > wo_item.transferred_qty)
- | (wo_item.required_qty > wo_item.consumed_qty)
- )
)
)
if check_production_plan:
query = query.where(wo.production_plan.isnotnull())
+ else:
+ query = query.where(
+ (wo.status.notin(["Stopped", "Completed", "Closed"]))
+ & (
+ (wo_item.required_qty > wo_item.transferred_qty)
+ | (wo_item.required_qty > wo_item.consumed_qty)
+ )
+ )
- if completed_production_plans:
- query = query.where(wo.production_plan.notin(completed_production_plans))
+ if non_completed_production_plans:
+ query = query.where(wo.production_plan.isin(non_completed_production_plans))
return query.run()[0][0] or 0.0
diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
index 34edb9d538..8729775dc2 100644
--- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
+++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
@@ -12,7 +12,7 @@ frappe.query_reports["BOM Operations Time"] = {
"options": "Item",
"get_query": () =>{
return {
- filters: { "disabled": 0, "is_stock_item": 1 }
+ filters: { "is_stock_item": 1 }
}
}
},
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index dcb421298d..c53cdf467d 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -17,5 +17,4 @@ Quality Management
Communication
Telephony
Bulk Transaction
-E-commerce
-Subcontracting
\ No newline at end of file
+Subcontracting
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 4c7d8e5221..3f9744d058 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -223,9 +223,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v13_0.fix_invoice_statuses
-erpnext.patches.v13_0.create_website_items #30-09-2021
-erpnext.patches.v13_0.populate_e_commerce_settings
-erpnext.patches.v13_0.make_homepage_products_website_items
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v14_0.update_opportunity_currency_fields
@@ -242,7 +239,6 @@ erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.healthcare_deprecation_warning
erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v13_0.update_category_in_ltds_certificate
-erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
@@ -257,6 +253,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v15_0.delete_taxjar_doctypes
+erpnext.patches.v15_0.delete_ecommerce_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
erpnext.patches.v15_0.saudi_depreciation_warning
@@ -277,8 +274,6 @@ erpnext.patches.v14_0.delete_datev_doctypes
erpnext.patches.v14_0.rearrange_company_fields
erpnext.patches.v13_0.update_sane_transfer_against
erpnext.patches.v14_0.migrate_cost_center_allocations
-erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
-erpnext.patches.v13_0.shopping_cart_to_ecommerce
erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype
@@ -288,7 +283,6 @@ erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
-erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v13_0.requeue_recoverable_reposts
erpnext.patches.v14_0.discount_accounting_separation
@@ -322,7 +316,7 @@ erpnext.patches.v14_0.update_closing_balances #14-07-2023
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
erpnext.patches.v14_0.update_subscription_details
-execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
+execute:frappe.delete_doc("Report", "Tax Detail", force=True)
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
@@ -344,6 +338,11 @@ erpnext.patches.v15_0.delete_woocommerce_settings_doctype
erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
erpnext.patches.v14_0.update_invoicing_period_in_subscription
execute:frappe.delete_doc("Page", "welcome-to-erpnext")
+erpnext.patches.v15_0.delete_payment_gateway_doctypes
+erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
+erpnext.patches.v15_0.update_sre_from_voucher_details
+erpnext.patches.v14_0.rename_over_order_allowance_field
+erpnext.patches.v14_0.migrate_delivery_stop_lock_field
erpnext.patches.v15_0.create_advance_payment_status
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py
deleted file mode 100644
index 9588e026d3..0000000000
--- a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import frappe
-
-
-def execute():
- frappe.reload_doctype("Landed Cost Taxes and Charges")
-
- company_account_map = frappe._dict(
- frappe.db.sql(
- """
- SELECT name, expenses_included_in_valuation from `tabCompany`
- """
- )
- )
-
- for company, account in company_account_map.items():
- frappe.db.sql(
- """
- UPDATE
- `tabLanded Cost Taxes and Charges` t, `tabLanded Cost Voucher` l
- SET
- t.expense_account = %s
- WHERE
- l.docstatus = 1
- AND l.company = %s
- AND t.parent = l.name
- """,
- (account, company),
- )
-
- frappe.db.sql(
- """
- UPDATE
- `tabLanded Cost Taxes and Charges` t, `tabStock Entry` s
- SET
- t.expense_account = %s
- WHERE
- s.docstatus = 1
- AND s.company = %s
- AND t.parent = s.name
- """,
- (account, company),
- )
diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
index efbb96c100..e53bdf8f19 100644
--- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
+++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
@@ -3,23 +3,24 @@ import frappe
def execute():
frappe.reload_doc("stock", "doctype", "quality_inspection_parameter")
+ params = set()
- # get all distinct parameters from QI readigs table
- reading_params = frappe.db.get_all(
- "Quality Inspection Reading", fields=["distinct specification"]
- )
- reading_params = [d.specification for d in reading_params]
+ # get all parameters from QI readings table
+ for (p,) in frappe.db.get_all(
+ "Quality Inspection Reading", fields=["specification"], as_list=True
+ ):
+ params.add(p.strip())
- # get all distinct parameters from QI Template as some may be unused in QI
- template_params = frappe.db.get_all(
- "Item Quality Inspection Parameter", fields=["distinct specification"]
- )
- template_params = [d.specification for d in template_params]
+ # get all parameters from QI Template as some may be unused in QI
+ for (p,) in frappe.db.get_all(
+ "Item Quality Inspection Parameter", fields=["specification"], as_list=True
+ ):
+ params.add(p.strip())
- params = list(set(reading_params + template_params))
+ # because db primary keys are case insensitive, so duplicates will cause an exception
+ params = set({x.casefold(): x for x in params}.values())
for parameter in params:
- if not frappe.db.exists("Quality Inspection Parameter", parameter):
- frappe.get_doc(
- {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
- ).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
+ ).insert(ignore_permissions=True)
diff --git a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py
deleted file mode 100644
index 1bac0fdbf0..0000000000
--- a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import json
-from typing import List, Union
-
-import frappe
-
-from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
-
-
-def execute():
- """
- Convert all Item links to Website Item link values in
- exisitng 'Item Card Group' Web Page Block data.
- """
- frappe.reload_doc("e_commerce", "web_template", "item_card_group")
-
- blocks = frappe.db.get_all(
- "Web Page Block",
- filters={"web_template": "Item Card Group"},
- fields=["parent", "web_template_values", "name"],
- )
-
- fields = generate_fields_to_edit()
-
- for block in blocks:
- web_template_value = json.loads(block.get("web_template_values"))
-
- for field in fields:
- item = web_template_value.get(field)
- if not item:
- continue
-
- if frappe.db.exists("Website Item", {"item_code": item}):
- website_item = frappe.db.get_value("Website Item", {"item_code": item})
- else:
- website_item = make_new_website_item(item)
-
- if website_item:
- web_template_value[field] = website_item
-
- frappe.db.set_value(
- "Web Page Block", block.name, "web_template_values", json.dumps(web_template_value)
- )
-
-
-def generate_fields_to_edit() -> List:
- fields = []
- for i in range(1, 13):
- fields.append(f"card_{i}_item") # fields like 'card_1_item', etc.
-
- return fields
-
-
-def make_new_website_item(item: str) -> Union[str, None]:
- try:
- doc = frappe.get_doc("Item", item)
- web_item = make_website_item(doc) # returns [website_item.name, item_name]
- return web_item[0]
- except Exception:
- doc.log_error("Website Item creation failed")
- return None
diff --git a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py
deleted file mode 100644
index 4ad572fdb0..0000000000
--- a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py
+++ /dev/null
@@ -1,94 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_field
-
-
-def execute():
- "Add Field Filters, that are not standard fields in Website Item, as Custom Fields."
-
- def move_table_multiselect_data(docfield):
- "Copy child table data (Table Multiselect) from Item to Website Item for a docfield."
- table_multiselect_data = get_table_multiselect_data(docfield)
- field = docfield.fieldname
-
- for row in table_multiselect_data:
- # add copied multiselect data rows in Website Item
- web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
- web_item_doc = frappe.get_doc("Website Item", web_item)
-
- child_doc = frappe.new_doc(docfield.options, parent_doc=web_item_doc, parentfield=field)
-
- for field in ["name", "creation", "modified", "idx"]:
- row[field] = None
-
- child_doc.update(row)
-
- child_doc.parenttype = "Website Item"
- child_doc.parent = web_item
-
- child_doc.insert()
-
- def get_table_multiselect_data(docfield):
- child_table = frappe.qb.DocType(docfield.options)
- item = frappe.qb.DocType("Item")
-
- table_multiselect_data = ( # query table data for field
- frappe.qb.from_(child_table)
- .join(item)
- .on(item.item_code == child_table.parent)
- .select(child_table.star)
- .where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))
- ).run(as_dict=True)
-
- return table_multiselect_data
-
- settings = frappe.get_doc("E Commerce Settings")
-
- if not (settings.enable_field_filters or settings.filter_fields):
- return
-
- item_meta = frappe.get_meta("Item")
- valid_item_fields = [
- df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
- ]
-
- web_item_meta = frappe.get_meta("Website Item")
- valid_web_item_fields = [
- df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
- ]
-
- for row in settings.filter_fields:
- # skip if illegal field
- if row.fieldname not in valid_item_fields:
- continue
-
- # if Item field is not in Website Item, add it as a custom field
- if row.fieldname not in valid_web_item_fields:
- df = item_meta.get_field(row.fieldname)
- create_custom_field(
- "Website Item",
- dict(
- owner="Administrator",
- fieldname=df.fieldname,
- label=df.label,
- fieldtype=df.fieldtype,
- options=df.options,
- description=df.description,
- read_only=df.read_only,
- no_copy=df.no_copy,
- insert_after="on_backorder",
- ),
- )
-
- # map field values
- if df.fieldtype == "Table MultiSelect":
- move_table_multiselect_data(df)
- else:
- frappe.db.sql( # nosemgrep
- """
- UPDATE `tabWebsite Item` wi, `tabItem` i
- SET wi.{0} = i.{0}
- WHERE wi.item_code = i.item_code
- """.format(
- row.fieldname
- )
- )
diff --git a/erpnext/patches/v13_0/create_website_items.py b/erpnext/patches/v13_0/create_website_items.py
deleted file mode 100644
index b010f0ecc6..0000000000
--- a/erpnext/patches/v13_0/create_website_items.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import frappe
-
-from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
-
-
-def execute():
- frappe.reload_doc("e_commerce", "doctype", "website_item")
- frappe.reload_doc("e_commerce", "doctype", "website_item_tabbed_section")
- frappe.reload_doc("e_commerce", "doctype", "website_offer")
- frappe.reload_doc("e_commerce", "doctype", "recommended_items")
- frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
- frappe.reload_doc("stock", "doctype", "item")
-
- item_fields = [
- "item_code",
- "item_name",
- "item_group",
- "stock_uom",
- "brand",
- "has_variants",
- "variant_of",
- "description",
- "weightage",
- ]
- web_fields_to_map = [
- "route",
- "slideshow",
- "website_image_alt",
- "website_warehouse",
- "web_long_description",
- "website_content",
- "website_image",
- "thumbnail",
- ]
-
- # get all valid columns (fields) from Item master DB schema
- item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) # nosemgrep
- item_table_fields = [d.get("Field") for d in item_table_fields]
-
- # prepare fields to query from Item, check if the web field exists in Item master
- web_query_fields = []
- for web_field in web_fields_to_map:
- if web_field in item_table_fields:
- web_query_fields.append(web_field)
- item_fields.append(web_field)
-
- # check if the filter fields exist in Item master
- or_filters = {}
- for field in ["show_in_website", "show_variant_in_website"]:
- if field in item_table_fields:
- or_filters[field] = 1
-
- if not web_query_fields or not or_filters:
- # web fields to map are not present in Item master schema
- # most likely a fresh installation that doesnt need this patch
- return
-
- items = frappe.db.get_all("Item", fields=item_fields, or_filters=or_filters)
- total_count = len(items)
-
- for count, item in enumerate(items, start=1):
- if frappe.db.exists("Website Item", {"item_code": item.item_code}):
- continue
-
- # make new website item from item (publish item)
- website_item = make_website_item(item, save=False)
- website_item.ranking = item.get("weightage")
-
- for field in web_fields_to_map:
- website_item.update({field: item.get(field)})
-
- website_item.save()
-
- # move Website Item Group & Website Specification table to Website Item
- for doctype in ("Website Item Group", "Item Website Specification"):
- frappe.db.set_value(
- doctype,
- {"parenttype": "Item", "parent": item.item_code}, # filters
- {"parenttype": "Website Item", "parent": website_item.name}, # value dict
- )
-
- if count % 20 == 0: # commit after every 20 items
- frappe.db.commit()
-
- frappe.utils.update_progress_bar("Creating Website Items", count, total_count)
diff --git a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
deleted file mode 100644
index 9197d86058..0000000000
--- a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import frappe
-
-
-def execute():
- if frappe.db.has_column("Item", "thumbnail"):
- website_item = frappe.qb.DocType("Website Item").as_("wi")
- item = frappe.qb.DocType("Item")
-
- frappe.qb.update(website_item).inner_join(item).on(website_item.item_code == item.item_code).set(
- website_item.thumbnail, item.thumbnail
- ).where(website_item.website_image.notnull() & website_item.thumbnail.isnull()).run()
diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py
deleted file mode 100644
index 50bfd358ea..0000000000
--- a/erpnext/patches/v13_0/make_homepage_products_website_items.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import frappe
-
-
-def execute():
- homepage = frappe.get_doc("Homepage")
-
- for row in homepage.products:
- web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name")
- if not web_item:
- continue
-
- row.item_code = web_item
-
- homepage.flags.ignore_mandatory = True
- homepage.save()
diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py
deleted file mode 100644
index ecf512b011..0000000000
--- a/erpnext/patches/v13_0/populate_e_commerce_settings.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import frappe
-from frappe.utils import cint
-
-
-def execute():
- frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
- frappe.reload_doc("portal", "doctype", "website_filter_field")
- frappe.reload_doc("portal", "doctype", "website_attribute")
-
- products_settings_fields = [
- "hide_variants",
- "products_per_page",
- "enable_attribute_filters",
- "enable_field_filters",
- ]
-
- shopping_cart_settings_fields = [
- "enabled",
- "show_attachments",
- "show_price",
- "show_stock_availability",
- "enable_variants",
- "show_contact_us_button",
- "show_quantity_in_website",
- "show_apply_coupon_code_in_website",
- "allow_items_not_in_stock",
- "company",
- "price_list",
- "default_customer_group",
- "quotation_series",
- "enable_checkout",
- "payment_success_url",
- "payment_gateway_account",
- "save_quotations_as_draft",
- ]
-
- settings = frappe.get_doc("E Commerce Settings")
-
- def map_into_e_commerce_settings(doctype, fields):
- singles = frappe.qb.DocType("Singles")
- query = (
- frappe.qb.from_(singles)
- .select(singles["field"], singles.value)
- .where((singles.doctype == doctype) & (singles["field"].isin(fields)))
- )
- data = query.run(as_dict=True)
-
- # {'enable_attribute_filters': '1', ...}
- mapper = {row.field: row.value for row in data}
-
- for key, value in mapper.items():
- value = cint(value) if (value and value.isdigit()) else value
- settings.update({key: value})
-
- settings.save()
-
- # shift data to E Commerce Settings
- map_into_e_commerce_settings("Products Settings", products_settings_fields)
- map_into_e_commerce_settings("Shopping Cart Settings", shopping_cart_settings_fields)
-
- # move filters and attributes tables to E Commerce Settings from Products Settings
- for doctype in ("Website Filter Field", "Website Attribute"):
- frappe.db.set_value(
- doctype,
- {"parent": "Products Settings"},
- {"parenttype": "E Commerce Settings", "parent": "E Commerce Settings"},
- update_modified=False,
- )
diff --git a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
deleted file mode 100644
index 35710a9bb4..0000000000
--- a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import click
-import frappe
-
-
-def execute():
-
- frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True)
- frappe.delete_doc("DocType", "Products Settings", ignore_missing=True)
- frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True)
-
- if frappe.db.get_single_value("E Commerce Settings", "enabled"):
- notify_users()
-
-
-def notify_users():
-
- click.secho(
- "Shopping cart and Product settings are merged into E-commerce settings.\n"
- "Checkout the documentation to learn more:"
- "https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce",
- fg="yellow",
- )
-
- note = frappe.new_doc("Note")
- note.title = "New E-Commerce Module"
- note.public = 1
- note.notify_on_login = 1
- note.content = """
"""
- note.insert(ignore_mandatory=True)
diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py
new file mode 100644
index 0000000000..8f77c35b12
--- /dev/null
+++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py
@@ -0,0 +1,7 @@
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ create_accounting_dimensions_for_doctype,
+)
+
+
+def execute():
+ create_accounting_dimensions_for_doctype(doctype="Sales Order Item")
diff --git a/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py
new file mode 100644
index 0000000000..c9ec1e113d
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_delivery_stop_lock_field.py
@@ -0,0 +1,7 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ if frappe.db.has_column("Delivery Stop", "lock"):
+ rename_field("Delivery Stop", "lock", "locked")
diff --git a/erpnext/patches/v14_0/rename_over_order_allowance_field.py b/erpnext/patches/v14_0/rename_over_order_allowance_field.py
new file mode 100644
index 0000000000..a81fe888c2
--- /dev/null
+++ b/erpnext/patches/v14_0/rename_over_order_allowance_field.py
@@ -0,0 +1,15 @@
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+ rename_field(
+ "Buying Settings",
+ "over_order_allowance",
+ "blanket_order_allowance",
+ )
+
+ rename_field(
+ "Selling Settings",
+ "over_order_allowance",
+ "blanket_order_allowance",
+ )
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index a53adf1a83..9a2a39fb78 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -11,6 +11,9 @@ def execute():
asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
for asset in assets:
+ if not asset_depreciation_schedules_map.get(asset.name):
+ continue
+
depreciation_schedules = asset_depreciation_schedules_map[asset.name]
for fb_row in asset_finance_books_map[asset.name]:
diff --git a/erpnext/patches/v15_0/delete_ecommerce_doctypes.py b/erpnext/patches/v15_0/delete_ecommerce_doctypes.py
new file mode 100644
index 0000000000..af0398782e
--- /dev/null
+++ b/erpnext/patches/v15_0/delete_ecommerce_doctypes.py
@@ -0,0 +1,30 @@
+import click
+import frappe
+
+
+def execute():
+ if "webshop" in frappe.get_installed_apps():
+ return
+
+ if not frappe.db.table_exists("Website Item"):
+ return
+
+ doctypes = [
+ "E Commerce Settings",
+ "Website Item",
+ "Recommended Items",
+ "Item Review",
+ "Wishlist Item",
+ "Wishlist",
+ "Website Offer",
+ "Website Item Tabbed Section",
+ ]
+
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
+
+ click.secho(
+ "ECommerce is renamed and moved to a separate app"
+ "Please install the app for ECommerce features: https://github.com/frappe/webshop",
+ fg="yellow",
+ )
diff --git a/erpnext/patches/v15_0/delete_payment_gateway_doctypes.py b/erpnext/patches/v15_0/delete_payment_gateway_doctypes.py
new file mode 100644
index 0000000000..959b065780
--- /dev/null
+++ b/erpnext/patches/v15_0/delete_payment_gateway_doctypes.py
@@ -0,0 +1,6 @@
+import frappe
+
+
+def execute():
+ for dt in ("GoCardless Settings", "GoCardless Mandate", "Mpesa Settings"):
+ frappe.delete_doc("DocType", dt, ignore_missing=True)
diff --git a/erpnext/patches/v15_0/update_sre_from_voucher_details.py b/erpnext/patches/v15_0/update_sre_from_voucher_details.py
new file mode 100644
index 0000000000..06ba553e3a
--- /dev/null
+++ b/erpnext/patches/v15_0/update_sre_from_voucher_details.py
@@ -0,0 +1,18 @@
+import frappe
+from frappe.query_builder.functions import IfNull
+
+
+def execute():
+ columns = frappe.db.get_table_columns("Stock Reservation Entry")
+
+ if set(["against_pick_list", "against_pick_list_item"]).issubset(set(columns)):
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ (
+ frappe.qb.update(sre)
+ .set(sre.from_voucher_type, "Pick List")
+ .set(sre.from_voucher_no, sre.against_pick_list)
+ .set(sre.from_voucher_detail_no, sre.against_pick_list_item)
+ .where(
+ (IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "")
+ )
+ ).run()
diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js
index 59f808a315..6797904424 100644
--- a/erpnext/portal/doctype/homepage/homepage.js
+++ b/erpnext/portal/doctype/homepage/homepage.js
@@ -19,12 +19,3 @@ frappe.ui.form.on('Homepage', {
});
},
});
-
-frappe.ui.form.on('Homepage Featured Product', {
- view: function(frm, cdt, cdn) {
- var child= locals[cdt][cdn];
- if (child.item_code && child.route) {
- window.open('/' + child.route, '_blank');
- }
- }
-});
diff --git a/erpnext/portal/doctype/homepage/homepage.json b/erpnext/portal/doctype/homepage/homepage.json
index 73f816d4d4..2b891f7268 100644
--- a/erpnext/portal/doctype/homepage/homepage.json
+++ b/erpnext/portal/doctype/homepage/homepage.json
@@ -15,10 +15,7 @@
"description",
"hero_image",
"slideshow",
- "hero_section",
- "products_section",
- "products_url",
- "products"
+ "hero_section"
],
"fields": [
{
@@ -86,30 +83,11 @@
"fieldtype": "Link",
"label": "Homepage Section",
"options": "Homepage Section"
- },
- {
- "fieldname": "products_section",
- "fieldtype": "Section Break",
- "label": "Products"
- },
- {
- "default": "/all-products",
- "fieldname": "products_url",
- "fieldtype": "Data",
- "label": "URL for \"All Products\""
- },
- {
- "description": "Products to be shown on website homepage",
- "fieldname": "products",
- "fieldtype": "Table",
- "label": "Products",
- "options": "Homepage Featured Product",
- "width": "40px"
}
],
"issingle": 1,
"links": [],
- "modified": "2021-02-18 13:29:29.531639",
+ "modified": "2022-12-19 21:10:29.127277",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage",
@@ -138,6 +116,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "company",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py
index 0d2e360788..c0a0c07d7d 100644
--- a/erpnext/portal/doctype/homepage/homepage.py
+++ b/erpnext/portal/doctype/homepage/homepage.py
@@ -12,26 +12,3 @@ class Homepage(Document):
if not self.description:
self.description = frappe._("This is an example website auto-generated from ERPNext")
delete_page_cache("home")
-
- def setup_items(self):
- for d in frappe.get_all(
- "Website Item",
- fields=["name", "item_name", "description", "website_image", "route"],
- filters={"published": 1},
- limit=3,
- ):
-
- doc = frappe.get_doc("Website Item", d.name)
- if not doc.route:
- # set missing route
- doc.save()
- self.append(
- "products",
- dict(
- item_code=d.name,
- item_name=d.item_name,
- description=d.description,
- image=d.website_image,
- route=d.route,
- ),
- )
diff --git a/erpnext/portal/doctype/homepage_featured_product/__init__.py b/erpnext/portal/doctype/homepage_featured_product/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
deleted file mode 100644
index 63789e35b5..0000000000
--- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
+++ /dev/null
@@ -1,118 +0,0 @@
-{
- "actions": [],
- "autoname": "hash",
- "creation": "2016-04-22 05:57:06.261401",
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "item_code",
- "col_break1",
- "item_name",
- "view",
- "section_break_5",
- "description",
- "column_break_7",
- "image",
- "thumbnail",
- "route"
- ],
- "fields": [
- {
- "bold": 1,
- "fieldname": "item_code",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Item",
- "oldfieldname": "item_code",
- "oldfieldtype": "Link",
- "options": "Website Item",
- "print_width": "150px",
- "reqd": 1,
- "search_index": 1,
- "width": "150px"
- },
- {
- "fieldname": "col_break1",
- "fieldtype": "Column Break"
- },
- {
- "fetch_from": "item_code.item_name",
- "fetch_if_empty": 1,
- "fieldname": "item_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Item Name",
- "oldfieldname": "item_name",
- "oldfieldtype": "Data",
- "print_hide": 1,
- "print_width": "150",
- "read_only": 1,
- "reqd": 1,
- "width": "150"
- },
- {
- "fieldname": "view",
- "fieldtype": "Button",
- "in_list_view": 1,
- "label": "View"
- },
- {
- "collapsible": 1,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "label": "Details"
- },
- {
- "fetch_from": "item_code.web_long_description",
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Small Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "fieldname": "column_break_7",
- "fieldtype": "Column Break"
- },
- {
- "fetch_from": "item_code.website_image",
- "fetch_if_empty": 1,
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "label": "Image"
- },
- {
- "fetch_from": "item_code.thumbnail",
- "fieldname": "thumbnail",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "label": "Thumbnail"
- },
- {
- "fetch_from": "item_code.route",
- "fieldname": "route",
- "fieldtype": "Small Text",
- "label": "route",
- "read_only": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2021-02-18 13:05:50.669311",
- "modified_by": "Administrator",
- "module": "Portal",
- "name": "Homepage Featured Product",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC"
-}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py
deleted file mode 100644
index c21461d631..0000000000
--- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HomepageFeaturedProduct(Document):
- pass
diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py
index c8b03e678b..903d4a6196 100644
--- a/erpnext/portal/utils.py
+++ b/erpnext/portal/utils.py
@@ -1,10 +1,4 @@
import frappe
-from frappe.utils.nestedset import get_root_of
-
-from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
- get_shopping_cart_settings,
-)
-from erpnext.e_commerce.shopping_cart.cart import get_debtors_account
def set_default_role(doc, method):
@@ -56,26 +50,7 @@ def create_customer_or_supplier():
party = frappe.new_doc(doctype)
fullname = frappe.utils.get_fullname(user)
- if doctype == "Customer":
- cart_settings = get_shopping_cart_settings()
-
- if cart_settings.enable_checkout:
- debtors_account = get_debtors_account(cart_settings)
- else:
- debtors_account = ""
-
- party.update(
- {
- "customer_name": fullname,
- "customer_type": "Individual",
- "customer_group": cart_settings.default_customer_group,
- "territory": get_root_of("Territory"),
- }
- )
-
- if debtors_account:
- party.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]})
- else:
+ if not doctype == "Customer":
party.update(
{
"supplier_name": fullname,
diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json
index 5102986f00..3300b7eb90 100644
--- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json
+++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json
@@ -24,6 +24,7 @@
},
{
"fetch_from": "task.subject",
+ "fetch_if_empty": 1,
"fieldname": "subject",
"fieldtype": "Text",
"in_list_view": 1,
@@ -31,7 +32,6 @@
"read_only": 1
},
{
- "fetch_from": "task.project",
"fieldname": "project",
"fieldtype": "Text",
"label": "Project",
@@ -40,7 +40,7 @@
],
"istable": 1,
"links": [],
- "modified": "2023-10-09 11:34:14.335853",
+ "modified": "2023-10-17 12:45:21.536165",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task Depends On",
diff --git a/erpnext/public/images/erpnext-favicon.svg b/erpnext/public/images/erpnext-favicon.svg
index 6bc6b2c2db..768e6e5514 100644
--- a/erpnext/public/images/erpnext-favicon.svg
+++ b/erpnext/public/images/erpnext-favicon.svg
@@ -1,5 +1,5 @@
-
+
diff --git a/erpnext/public/images/erpnext-logo.png b/erpnext/public/images/erpnext-logo.png
index 3090727d8f..b4c27493e2 100644
Binary files a/erpnext/public/images/erpnext-logo.png and b/erpnext/public/images/erpnext-logo.png differ
diff --git a/erpnext/public/images/erpnext-logo.svg b/erpnext/public/images/erpnext-logo.svg
index 6bc6b2c2db..768e6e5514 100644
--- a/erpnext/public/images/erpnext-logo.svg
+++ b/erpnext/public/images/erpnext-logo.svg
@@ -1,5 +1,5 @@
-
+
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index a2e4bdacac..7879173cd1 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -30,7 +30,6 @@ erpnext.accounts.taxes = {
filters: {
"account_type": account_type,
"company": doc.company,
- "disabled": 0
}
}
});
@@ -116,7 +115,7 @@ erpnext.accounts.taxes = {
account_head: function(frm, cdt, cdn) {
let d = locals[cdt][cdn];
- if (doc.docstatus == 1) {
+ if (d.docstatus == 1) {
// Should not trigger any changes on change post submit
return;
}
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 70b70c3d28..6b613ce9ec 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -193,7 +193,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
frappe.flags.round_off_applicable_accounts = [];
if (me.frm.doc.company) {
- return frappe.call({
+ frappe.call({
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
"args": {
"company": me.frm.doc.company,
@@ -206,6 +206,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
});
}
+
+ frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
+ .then((round_row_wise_tax) => {
+ frappe.flags.round_row_wise_tax = round_row_wise_tax;
+ })
}
determine_exclusive_rate() {
@@ -346,6 +351,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
// tax_amount represents the amount of tax for the current step
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
+ if (frappe.flags.round_row_wise_tax) {
+ current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax));
+ }
// Adjust divisional loss to the last item
if (tax.charge_type == "Actual") {
@@ -480,8 +488,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate;
- if (tax_detail && tax_detail[key])
- item_wise_tax_amount += tax_detail[key][1];
+ if (frappe.flags.round_row_wise_tax) {
+ item_wise_tax_amount = flt(item_wise_tax_amount, precision("tax_amount", tax));
+ if (tax_detail && tax_detail[key]) {
+ item_wise_tax_amount += flt(tax_detail[key][1], precision("tax_amount", tax));
+ }
+ } else {
+ if (tax_detail && tax_detail[key])
+ item_wise_tax_amount += tax_detail[key][1];
+ }
tax_detail[key] = [tax_rate, flt(item_wise_tax_amount, precision("base_tax_amount", tax))];
}
diff --git a/erpnext/public/js/customer_reviews.js b/erpnext/public/js/customer_reviews.js
deleted file mode 100644
index e13ded6b48..0000000000
--- a/erpnext/public/js/customer_reviews.js
+++ /dev/null
@@ -1,138 +0,0 @@
-$(() => {
- class CustomerReviews {
- constructor() {
- this.bind_button_actions();
- this.start = 0;
- this.page_length = 10;
- }
-
- bind_button_actions() {
- this.write_review();
- this.view_more();
- }
-
- write_review() {
- //TODO: make dialog popup on stray page
- $('.page_content').on('click', '.btn-write-review', (e) => {
- // Bind action on write a review button
- const $btn = $(e.currentTarget);
-
- let d = new frappe.ui.Dialog({
- title: __("Write a Review"),
- fields: [
- {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1},
- {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1},
- {fieldtype: "Section Break"},
- {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"}
- ],
- primary_action: function() {
- let data = d.get_values();
- frappe.call({
- method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
- args: {
- web_item: $btn.attr('data-web-item'),
- title: data.title,
- rating: data.rating,
- comment: data.comment
- },
- freeze: true,
- freeze_message: __("Submitting Review ..."),
- callback: (r) => {
- if (!r.exc) {
- frappe.msgprint({
- message: __("Thank you for submitting your review"),
- title: __("Review Submitted"),
- indicator: "green"
- });
- d.hide();
- location.reload();
- }
- }
- });
- },
- primary_action_label: __('Submit')
- });
- d.show();
- });
- }
-
- view_more() {
- $('.page_content').on('click', '.btn-view-more', (e) => {
- // Bind action on view more button
- const $btn = $(e.currentTarget);
- $btn.prop('disabled', true);
-
- this.start += this.page_length;
- let me = this;
-
- frappe.call({
- method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews",
- args: {
- web_item: $btn.attr('data-web-item'),
- start: me.start,
- end: me.page_length
- },
- callback: (result) => {
- if (result.message) {
- let res = result.message;
- me.get_user_review_html(res.reviews);
-
- $btn.prop('disabled', false);
- if (res.total_reviews <= (me.start + me.page_length)) {
- $btn.hide();
- }
-
- }
- }
- });
- });
-
- }
-
- get_user_review_html(reviews) {
- let me = this;
- let $content = $('.user-reviews');
-
- reviews.forEach((review) => {
- $content.append(`
-
-
-
- ${__(review.review_title)}
-
-
- ${me.get_review_stars(review.rating)}
-
-
-
-
-
- ${__(review.comment)}
-
-
-
- ${__(review.customer)}
-
- ${__(review.published_on)}
-
-
- `);
- });
- }
-
- get_review_stars(rating) {
- let stars = ``;
- for (let i = 1; i < 6; i++) {
- let fill_class = i <= rating ? 'star-click' : '';
- stars += `
-
-
-
- `;
- }
- return stars;
- }
- }
-
- new CustomerReviews();
-});
\ No newline at end of file
diff --git a/erpnext/public/js/erpnext-web.bundle.js b/erpnext/public/js/erpnext-web.bundle.js
index cbe899dc06..45c6a648ec 100644
--- a/erpnext/public/js/erpnext-web.bundle.js
+++ b/erpnext/public/js/erpnext-web.bundle.js
@@ -1,8 +1 @@
import "./website_utils";
-import "./wishlist";
-import "./shopping_cart";
-import "./customer_reviews";
-import "../../e_commerce/product_ui/list";
-import "../../e_commerce/product_ui/views";
-import "../../e_commerce/product_ui/grid";
-import "../../e_commerce/product_ui/search";
\ No newline at end of file
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
deleted file mode 100644
index d14740c106..0000000000
--- a/erpnext/public/js/shopping_cart.js
+++ /dev/null
@@ -1,243 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
-
-// shopping cart
-frappe.provide("erpnext.e_commerce.shopping_cart");
-var shopping_cart = erpnext.e_commerce.shopping_cart;
-
-var getParams = function (url) {
- var params = [];
- var parser = document.createElement('a');
- parser.href = url;
- var query = parser.search.substring(1);
- var vars = query.split('&');
- for (var i = 0; i < vars.length; i++) {
- var pair = vars[i].split('=');
- params[pair[0]] = decodeURIComponent(pair[1]);
- }
- return params;
-};
-
-frappe.ready(function() {
- var full_name = frappe.session && frappe.session.user_fullname;
- // update user
- if(full_name) {
- $('.navbar li[data-label="User"] a')
- .html('
' + full_name);
- }
- // set coupon code and sales partner code
-
- var url_args = getParams(window.location.href);
-
- var referral_coupon_code = url_args['cc'];
- var referral_sales_partner = url_args['sp'];
-
- var d = new Date();
- // expires within 30 minutes
- d.setTime(d.getTime() + (0.02 * 24 * 60 * 60 * 1000));
- var expires = "expires="+d.toUTCString();
- if (referral_coupon_code) {
- document.cookie = "referral_coupon_code=" + referral_coupon_code + ";" + expires + ";path=/";
- }
- if (referral_sales_partner) {
- document.cookie = "referral_sales_partner=" + referral_sales_partner + ";" + expires + ";path=/";
- }
- referral_coupon_code=frappe.get_cookie("referral_coupon_code");
- referral_sales_partner=frappe.get_cookie("referral_sales_partner");
-
- if (referral_coupon_code && $(".tot_quotation_discount").val()==undefined ) {
- $(".txtcoupon").val(referral_coupon_code);
- }
- if (referral_sales_partner) {
- $(".txtreferral_sales_partner").val(referral_sales_partner);
- }
-
- // update login
- shopping_cart.show_shoppingcart_dropdown();
- shopping_cart.set_cart_count();
- shopping_cart.show_cart_navbar();
-});
-
-$.extend(shopping_cart, {
- show_shoppingcart_dropdown: function() {
- $(".shopping-cart").on('shown.bs.dropdown', function() {
- if (!$('.shopping-cart-menu .cart-container').length) {
- return frappe.call({
- method: 'erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu',
- callback: function(r) {
- if (r.message) {
- $('.shopping-cart-menu').html(r.message);
- }
- }
- });
- }
- });
- },
-
- update_cart: function(opts) {
- if (frappe.session.user==="Guest") {
- if (localStorage) {
- localStorage.setItem("last_visited", window.location.pathname);
- }
- frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
- window.location.href = res.message || "/login";
- });
- } else {
- shopping_cart.freeze();
- return frappe.call({
- type: "POST",
- method: "erpnext.e_commerce.shopping_cart.cart.update_cart",
- args: {
- item_code: opts.item_code,
- qty: opts.qty,
- additional_notes: opts.additional_notes !== undefined ? opts.additional_notes : undefined,
- with_items: opts.with_items || 0
- },
- btn: opts.btn,
- callback: function(r) {
- shopping_cart.unfreeze();
- shopping_cart.set_cart_count(true);
- if(opts.callback)
- opts.callback(r);
- }
- });
- }
- },
-
- set_cart_count: function(animate=false) {
- $(".intermediate-empty-cart").remove();
-
- var cart_count = frappe.get_cookie("cart_count");
- if(frappe.session.user==="Guest") {
- cart_count = 0;
- }
-
- if(cart_count) {
- $(".shopping-cart").toggleClass('hidden', false);
- }
-
- var $cart = $('.cart-icon');
- var $badge = $cart.find("#cart-count");
-
- if(parseInt(cart_count) === 0 || cart_count === undefined) {
- $cart.css("display", "none");
- $(".cart-tax-items").hide();
- $(".btn-place-order").hide();
- $(".cart-payment-addresses").hide();
-
- let intermediate_empty_cart_msg = `
-
- ${ __("Cart is Empty") }
-
- `;
- $(".cart-table").after(intermediate_empty_cart_msg);
- }
- else {
- $cart.css("display", "inline");
- $("#cart-count").text(cart_count);
- }
-
- if(cart_count) {
- $badge.html(cart_count);
-
- if (animate) {
- $cart.addClass("cart-animate");
- setTimeout(() => {
- $cart.removeClass("cart-animate");
- }, 500);
- }
- } else {
- $badge.remove();
- }
- },
-
- shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) {
- shopping_cart.update_cart({
- item_code,
- qty,
- additional_notes,
- with_items: 1,
- btn: this,
- callback: function(r) {
- if(!r.exc) {
- $(".cart-items").html(r.message.items);
- $(".cart-tax-items").html(r.message.total);
- $(".payment-summary").html(r.message.taxes_and_totals);
- shopping_cart.set_cart_count();
-
- if (cart_dropdown != true) {
- $(".cart-icon").hide();
- }
- }
- },
- });
- },
-
- show_cart_navbar: function () {
- frappe.call({
- method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled",
- callback: function(r) {
- $(".shopping-cart").toggleClass('hidden', r.message ? false : true);
- }
- });
- },
-
- toggle_button_class(button, remove, add) {
- button.removeClass(remove);
- button.addClass(add);
- },
-
- bind_add_to_cart_action() {
- $('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
- const $btn = $(e.currentTarget);
- $btn.prop('disabled', true);
-
- if (frappe.session.user==="Guest") {
- if (localStorage) {
- localStorage.setItem("last_visited", window.location.pathname);
- }
- frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
- window.location.href = res.message || "/login";
- });
- return;
- }
-
- $btn.addClass('hidden');
- $btn.closest('.cart-action-container').addClass('d-flex');
- $btn.parent().find('.go-to-cart').removeClass('hidden');
- $btn.parent().find('.go-to-cart-grid').removeClass('hidden');
- $btn.parent().find('.cart-indicator').removeClass('hidden');
-
- const item_code = $btn.data('item-code');
- erpnext.e_commerce.shopping_cart.update_cart({
- item_code,
- qty: 1
- });
-
- });
- },
-
- freeze() {
- if (window.location.pathname !== "/cart") return;
-
- if (!$('#freeze').length) {
- let freeze = $('
')
- .appendTo("body");
-
- setTimeout(function() {
- freeze.addClass("show");
- }, 1);
- } else {
- $("#freeze").addClass("show");
- }
- },
-
- unfreeze() {
- if ($('#freeze').length) {
- let freeze = $('#freeze').removeClass("show");
- setTimeout(function() {
- freeze.remove();
- }, 1);
- }
- }
-});
diff --git a/erpnext/public/js/utils/item_selector.js b/erpnext/public/js/utils/item_selector.js
index 9fc264086a..e74d291acd 100644
--- a/erpnext/public/js/utils/item_selector.js
+++ b/erpnext/public/js/utils/item_selector.js
@@ -97,14 +97,14 @@ erpnext.ItemSelector = class ItemSelector {
}
var me = this;
- frappe.link_search("Item", args, function(r) {
- $.each(r.values, function(i, d) {
+ frappe.link_search("Item", args, function(results) {
+ $.each(results, function(i, d) {
if(!d.image) {
d.abbr = frappe.get_abbr(d.item_name);
d.color = frappe.get_palette(d.item_name);
}
});
- me.dialog.results.html(frappe.render_template('item_selector', {'data':r.values}));
+ me.dialog.results.html(frappe.render_template('item_selector', {'data': results}));
});
}
};
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
deleted file mode 100644
index f6599e9f6d..0000000000
--- a/erpnext/public/js/wishlist.js
+++ /dev/null
@@ -1,204 +0,0 @@
-frappe.provide("erpnext.e_commerce.wishlist");
-var wishlist = erpnext.e_commerce.wishlist;
-
-frappe.provide("erpnext.e_commerce.shopping_cart");
-var shopping_cart = erpnext.e_commerce.shopping_cart;
-
-$.extend(wishlist, {
- set_wishlist_count: function(animate=false) {
- // set badge count for wishlist icon
- var wish_count = frappe.get_cookie("wish_count");
- if (frappe.session.user==="Guest") {
- wish_count = 0;
- }
-
- if (wish_count) {
- $(".wishlist").toggleClass('hidden', false);
- }
-
- var $wishlist = $('.wishlist-icon');
- var $badge = $wishlist.find("#wish-count");
-
- if (parseInt(wish_count) === 0 || wish_count === undefined) {
- $wishlist.css("display", "none");
- } else {
- $wishlist.css("display", "inline");
- }
- if (wish_count) {
- $badge.html(wish_count);
- if (animate) {
- $wishlist.addClass('cart-animate');
- setTimeout(() => {
- $wishlist.removeClass('cart-animate');
- }, 500);
- }
- } else {
- $badge.remove();
- }
- },
-
- bind_move_to_cart_action: function() {
- // move item to cart from wishlist
- $('.page_content').on("click", ".btn-add-to-cart", (e) => {
- const $move_to_cart_btn = $(e.currentTarget);
- let item_code = $move_to_cart_btn.data("item-code");
-
- shopping_cart.shopping_cart_update({
- item_code,
- qty: 1,
- cart_dropdown: true
- });
-
- let success_action = function() {
- const $card_wrapper = $move_to_cart_btn.closest(".wishlist-card");
- $card_wrapper.addClass("wish-removed");
- };
- let args = { item_code: item_code };
- this.add_remove_from_wishlist("remove", args, success_action, null, true);
- });
- },
-
- bind_remove_action: function() {
- // remove item from wishlist
- let me = this;
-
- $('.page_content').on("click", ".remove-wish", (e) => {
- const $remove_wish_btn = $(e.currentTarget);
- let item_code = $remove_wish_btn.data("item-code");
-
- let success_action = function() {
- const $card_wrapper = $remove_wish_btn.closest(".wishlist-card");
- $card_wrapper.addClass("wish-removed");
- if (frappe.get_cookie("wish_count") == 0) {
- $(".page_content").empty();
- me.render_empty_state();
- }
- };
- let args = { item_code: item_code };
- this.add_remove_from_wishlist("remove", args, success_action);
- });
- },
-
- bind_wishlist_action() {
- // 'wish'('like') or 'unwish' item in product listing
- $('.page_content').on('click', '.like-action, .like-action-list', (e) => {
- const $btn = $(e.currentTarget);
- this.wishlist_action($btn);
- });
- },
-
- wishlist_action(btn) {
- const $wish_icon = btn.find('.wish-icon');
- let me = this;
-
- if (frappe.session.user==="Guest") {
- if (localStorage) {
- localStorage.setItem("last_visited", window.location.pathname);
- }
- this.redirect_guest();
- return;
- }
-
- let success_action = function() {
- erpnext.e_commerce.wishlist.set_wishlist_count(true);
- };
-
- if ($wish_icon.hasClass('wished')) {
- // un-wish item
- btn.removeClass("like-animate");
- btn.addClass("like-action-wished");
- this.toggle_button_class($wish_icon, 'wished', 'not-wished');
-
- let args = { item_code: btn.data('item-code') };
- let failure_action = function() {
- me.toggle_button_class($wish_icon, 'not-wished', 'wished');
- };
- this.add_remove_from_wishlist("remove", args, success_action, failure_action);
- } else {
- // wish item
- btn.addClass("like-animate");
- btn.addClass("like-action-wished");
- this.toggle_button_class($wish_icon, 'not-wished', 'wished');
-
- let args = {item_code: btn.data('item-code')};
- let failure_action = function() {
- me.toggle_button_class($wish_icon, 'wished', 'not-wished');
- };
- this.add_remove_from_wishlist("add", args, success_action, failure_action);
- }
- },
-
- toggle_button_class(button, remove, add) {
- button.removeClass(remove);
- button.addClass(add);
- },
-
- add_remove_from_wishlist(action, args, success_action, failure_action, async=false) {
- /* AJAX call to add or remove Item from Wishlist
- action: "add" or "remove"
- args: args for method (item_code, price, formatted_price),
- success_action: method to execute on successs,
- failure_action: method to execute on failure,
- async: make call asynchronously (true/false). */
- if (frappe.session.user==="Guest") {
- if (localStorage) {
- localStorage.setItem("last_visited", window.location.pathname);
- }
- this.redirect_guest();
- } else {
- let method = "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist";
- if (action === "remove") {
- method = "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist";
- }
-
- frappe.call({
- async: async,
- type: "POST",
- method: method,
- args: args,
- callback: function (r) {
- if (r.exc) {
- if (failure_action && (typeof failure_action === 'function')) {
- failure_action();
- }
- frappe.msgprint({
- message: __("Sorry, something went wrong. Please refresh."),
- indicator: "red", title: __("Note")
- });
- } else if (success_action && (typeof success_action === 'function')) {
- success_action();
- }
- }
- });
- }
- },
-
- redirect_guest() {
- frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
- window.location.href = res.message || "/login";
- });
- },
-
- render_empty_state() {
- $(".page_content").append(`
-
-
-
-
-
${ __('Wishlist is empty !') }
-
- `);
- }
-
-});
-
-frappe.ready(function() {
- if (window.location.pathname !== "/wishlist") {
- $(".wishlist").toggleClass('hidden', true);
- wishlist.set_wishlist_count();
- } else {
- wishlist.bind_move_to_cart_action();
- wishlist.bind_remove_action();
- }
-
-});
\ No newline at end of file
diff --git a/erpnext/public/scss/erpnext-web.bundle.scss b/erpnext/public/scss/erpnext-web.bundle.scss
index 6ef1892a3d..18d7c6cf4e 100644
--- a/erpnext/public/scss/erpnext-web.bundle.scss
+++ b/erpnext/public/scss/erpnext-web.bundle.scss
@@ -1,2 +1 @@
-@import "./shopping_cart";
@import "./website";
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
deleted file mode 100644
index 6ae464d2c2..0000000000
--- a/erpnext/public/scss/shopping_cart.scss
+++ /dev/null
@@ -1,1381 +0,0 @@
-@import "frappe/public/scss/common/mixins";
-
-:root {
- --green-info: #38A160;
- --product-bg-color: white;
- --body-bg-color: var(--gray-50);
-}
-
-body.product-page {
- background: var(--body-bg-color);
-}
-
-.item-breadcrumbs {
- .breadcrumb-container {
- a {
- color: var(--gray-900);
- }
- }
-}
-
-.carousel-control {
- height: 42px;
- width: 42px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08), 0px 1px 2px 1px rgba(0, 0, 0, 0.06);
- border-radius: 100px;
-}
-
-.carousel-control-prev,
-.carousel-control-next {
- opacity: 1;
- width: 8%;
-
- @media (max-width: 1200px) {
- width: 10%;
- }
- @media (max-width: 768px) {
- width: 15%;
- }
-}
-
-.carousel-body {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-
-.carousel-content {
- max-width: 400px;
- margin-left: 5rem;
- margin-right: 5rem;
-}
-
-.card {
- border: none;
-}
-
-.product-category-section {
- .card:hover {
- box-shadow: 0px 16px 45px 6px rgba(0, 0, 0, 0.08), 0px 8px 10px -10px rgba(0, 0, 0, 0.04);
- }
-
- .card-grid {
- display: grid;
- grid-gap: 15px;
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
- }
-}
-
-.no-image-item {
- height: 340px;
- width: 340px;
- background: var(--gray-100);
- border-radius: var(--border-radius);
- font-size: 2rem;
- color: var(--gray-500);
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.item-card-group-section {
- .card {
- height: 100%;
- align-items: center;
- justify-content: center;
-
- &:hover {
- box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);
- transition: box-shadow 400ms;
- }
- }
-
- .card:hover, .card:focus-within {
- .btn-add-to-cart-list {
- visibility: visible;
- }
- .like-action {
- visibility: visible;
- }
- .btn-explore-variants {
- visibility: visible;
- }
- }
-
-
- .card-img-container {
- height: 210px;
- width: 100%;
- }
-
- .card-img {
- max-height: 210px;
- object-fit: contain;
- margin-top: 1.25rem;
- }
-
- .no-image {
- @include flex(flex, center, center, null);
- height: 220px;
- background: var(--gray-100);
- width: 100%;
- border-radius: var(--border-radius) var(--border-radius) 0 0;
- font-size: 2rem;
- color: var(--gray-500);
- }
-
- .no-image-list {
- @include flex(flex, center, center, null);
- height: 150px;
- background: var(--gray-100);
- border-radius: var(--border-radius);
- font-size: 2rem;
- color: var(--gray-500);
- margin-top: 15px;
- margin-bottom: 15px;
- }
-
- .card-body-flex {
- display: flex;
- flex-direction: column;
- }
-
- .product-title {
- font-size: 14px;
- color: var(--gray-800);
- font-weight: 500;
- }
-
- .product-description {
- font-size: 12px;
- color: var(--text-color);
- margin: 20px 0;
- display: -webkit-box;
- -webkit-line-clamp: 6;
- -webkit-box-orient: vertical;
-
- p {
- margin-bottom: 0.5rem;
- }
- }
-
- .product-category {
- font-size: 13px;
- color: var(--text-muted);
- margin: var(--margin-sm) 0;
- }
-
- .product-price {
- font-size: 18px;
- font-weight: 600;
- color: var(--text-color);
- margin: var(--margin-sm) 0;
- margin-bottom: auto !important;
-
- .striked-price {
- font-weight: 500;
- font-size: 15px;
- color: var(--gray-500);
- }
- }
-
- .product-info-green {
- color: var(--green-info);
- font-weight: 600;
- }
-
- .item-card {
- padding: var(--padding-sm);
- min-width: 300px;
- }
-
- .wishlist-card {
- padding: var(--padding-sm);
- min-width: 260px;
- .card-body-flex {
- display: flex;
- flex-direction: column;
- }
- }
-}
-
-#products-list-area, #products-grid-area {
- padding: 0 5px;
-}
-
-.list-row {
- background-color: white;
- padding-bottom: 1rem;
- padding-top: 1.5rem !important;
- border-radius: 8px;
- border-bottom: 1px solid var(--gray-50);
-
- &:hover, &:focus-within {
- box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);
- transition: box-shadow 400ms;
-
- .btn-add-to-cart-list {
- visibility: visible;
- }
- .like-action-list {
- visibility: visible;
- }
- .btn-explore-variants {
- visibility: visible;
- }
- }
-
- .product-code {
- padding-top: 0 !important;
- }
-
- .btn-explore-variants {
- min-width: 135px;
- max-height: 30px;
- float: right;
- padding: 0.25rem 1rem;
- }
-}
-
-[data-doctype="Item Group"],
-#page-index {
- .page-header {
- font-size: 20px;
- font-weight: 700;
- color: var(--text-color);
- }
-
- .filters-section {
- .title-section {
- border-bottom: 1px solid var(--table-border-color);
- }
-
- .filter-title {
- font-weight: 500;
- }
-
- .clear-filters {
- font-size: 13px;
- }
-
- .filter-lookup-input {
- background-color: white;
- border: 1px solid var(--gray-300);
-
- &:focus {
- border: 1px solid var(--primary);
- }
- }
-
- .filter-label {
- font-size: 11px;
- font-weight: 600;
- color: var(--gray-700);
- text-transform: uppercase;
- }
-
- .filter-block {
- border-bottom: 1px solid var(--table-border-color);
- }
-
- .checkbox {
- .label-area {
- font-size: 13px;
- color: var(--gray-800);
- }
- }
- }
-}
-
-.product-filter {
- width: 14px !important;
- height: 14px !important;
-}
-
-.discount-filter {
- &:before {
- width: 14px !important;
- height: 14px !important;
- }
-}
-
-.list-image {
- border: none !important;
- overflow: hidden;
- max-height: 200px;
- background-color: white;
-}
-
-.product-container {
- @include card($padding: var(--padding-md));
- background-color: var(--product-bg-color) !important;
- min-height: fit-content;
-
- .product-details {
- max-width: 50%;
-
- .btn-add-to-cart {
- font-size: 14px;
- }
- }
-
- &.item-main {
- .product-image {
- width: 100%;
- }
- }
-
- .expand {
- max-width: 100% !important; // expand in absence of slideshow
- }
-
- @media (max-width: 789px) {
- .product-details {
- max-width: 90% !important;
-
- .btn-add-to-cart {
- font-size: 14px;
- }
- }
- }
-
- .btn-add-to-wishlist {
- svg use {
- --icon-stroke: #F47A7A;
- }
- }
-
- .btn-view-in-wishlist {
- svg use {
- fill: #F47A7A;
- --icon-stroke: none;
- }
- }
-
- .product-title {
- font-size: 16px;
- font-weight: 600;
- color: var(--text-color);
- padding: 0 !important;
- }
-
- .product-description {
- font-size: 13px;
- color: var(--gray-800);
- }
-
- .product-image {
- border-color: var(--table-border-color) !important;
- padding: 15px;
-
- @media (max-width: var(--md-width)) {
- height: 300px;
- width: 300px;
- }
-
- @media (min-width: var(--lg-width)) {
- height: 350px;
- width: 350px;
- }
-
- img {
- object-fit: contain;
- }
- }
-
- .item-slideshow {
-
- @media (max-width: var(--md-width)) {
- max-height: 320px;
- }
-
- @media (min-width: var(--lg-width)) {
- max-height: 430px;
- }
-
- overflow: auto;
- }
-
- .item-slideshow-image {
- height: 4rem;
- width: 6rem;
- object-fit: contain;
- padding: 0.5rem;
- border: 1px solid var(--table-border-color);
- border-radius: 4px;
- cursor: pointer;
-
- &:hover, &.active {
- border-color: var(--primary);
- }
- }
-
- .item-cart {
- .product-price {
- font-size: 22px;
- color: var(--text-color);
- font-weight: 600;
-
- .formatted-price {
- color: var(--text-muted);
- font-size: 14px;
- }
- }
-
- .no-stock {
- font-size: var(--text-base);
- }
-
- .offers-heading {
- font-size: 16px !important;
- color: var(--text-color);
- .tag-icon {
- --icon-stroke: var(--gray-500);
- }
- }
-
- .w-30-40 {
- width: 30%;
-
- @media (max-width: 992px) {
- width: 40%;
- }
- }
- }
-
- .tab-content {
- font-size: 14px;
- }
-}
-
-// Item Recommendations
-.recommended-item-section {
- padding-right: 0;
-
- .recommendation-header {
- font-size: 16px;
- font-weight: 500
- }
-
- .recommendation-container {
- padding: .5rem;
- min-height: 0px;
-
- .r-item-image {
- min-height: 100px;
- width: 40%;
-
- .r-product-image {
- padding: 2px 15px;
- }
-
- .no-image-r-item {
- display: flex; justify-content: center;
- background-color: var(--gray-200);
- align-items: center;
- color: var(--gray-400);
- margin-top: .15rem;
- border-radius: 6px;
- height: 100%;
- font-size: 24px;
- }
- }
-
- .r-item-info {
- font-size: 14px;
- padding-right: 0;
- padding-left: 10px;
- width: 60%;
-
- a {
- color: var(--gray-800);
- font-weight: 400;
- }
-
- .item-price {
- font-size: 15px;
- font-weight: 600;
- color: var(--text-color);
- }
-
- .striked-item-price {
- font-weight: 500;
- color: var(--gray-500);
- }
- }
- }
-}
-
-.product-code {
- padding: .5rem 0;
- color: var(--text-muted);
- font-size: 14px;
- .product-item-group {
- padding-right: .25rem;
- border-right: solid 1px var(--text-muted);
- }
-
- .product-item-code {
- padding-left: .5rem;
- }
-}
-
-.item-configurator-dialog {
- .modal-body {
- padding-bottom: var(--padding-xl);
-
- .status-area {
- .alert {
- padding: var(--padding-xs) var(--padding-sm);
- font-size: var(--text-sm);
- }
- }
-
- .form-layout {
- max-height: 50vh;
- overflow-y: auto;
- }
-
- .section-body {
- .form-column {
- .form-group {
- .control-label {
- font-size: var(--text-md);
- color: var(--gray-700);
- }
-
- .help-box {
- margin-top: 2px;
- font-size: var(--text-sm);
- }
- }
- }
- }
- }
-}
-
-.item-group-slideshow {
-
- .carousel-inner.rounded-carousel {
- border-radius: var(--card-border-radius);
- }
-}
-
-.sub-category-container {
- padding-bottom: .5rem;
- margin-bottom: 1.25rem;
- border-bottom: 1px solid var(--table-border-color);
-
- .heading {
- color: var(--gray-500);
- }
-}
-
-.scroll-categories {
- .category-pill {
- display: inline-block;
- width: fit-content;
- padding: 6px 12px;
- margin-bottom: 8px;
- background-color: #ecf5fe;
- font-size: 14px;
- border-radius: 18px;
- color: var(--blue-500);
- }
-}
-
-
-.shopping-badge {
- position: relative;
- top: -10px;
- left: -12px;
- background: var(--red-600);
- align-items: center;
- height: 16px;
- font-size: 10px;
- border-radius: 50%;
-}
-
-
-.cart-animate {
- animation: wiggle 0.5s linear;
-}
-@keyframes wiggle {
- 8%,
- 41% {
- transform: translateX(-10px);
- }
- 25%,
- 58% {
- transform: translate(10px);
- }
- 75% {
- transform: translate(-5px);
- }
- 92% {
- transform: translate(5px);
- }
- 0%,
- 100% {
- transform: translate(0);
- }
-}
-
-.total-discount {
- font-size: 14px;
- color: var(--primary-color) !important;
-}
-
-#page-cart {
- .shopping-cart-header {
- font-weight: bold;
- }
-
- .cart-container {
- color: var(--text-color);
-
- .frappe-card {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: fit-content;
- }
-
- .cart-items-header {
- font-weight: 600;
- }
-
- .cart-table {
- tr {
- margin-bottom: 1rem;
- }
-
- th, tr, td {
- border-color: var(--border-color);
- border-width: 1px;
- }
-
- th {
- font-weight: normal;
- font-size: 13px;
- color: var(--text-muted);
- padding: var(--padding-sm) 0;
- }
-
- td {
- padding: var(--padding-sm) 0;
- color: var(--text-color);
- }
-
- .cart-item-image {
- width: 20%;
- min-width: 100px;
- img {
- max-height: 112px;
- }
- }
-
- .cart-items {
- .item-title {
- width: 80%;
- font-size: 14px;
- font-weight: 500;
- color: var(--text-color);
- }
-
- .item-subtitle {
- color: var(--text-muted);
- font-size: 13px;
- }
-
- .item-subtotal {
- font-size: 14px;
- font-weight: 500;
- }
-
- .sm-item-subtotal {
- font-size: 14px;
- font-weight: 500;
- display: none;
-
- @media (max-width: 992px) {
- display: unset !important;
- }
- }
-
- .item-rate {
- font-size: 13px;
- color: var(--text-muted);
- }
-
- .free-tag {
- padding: 4px 8px;
- border-radius: 4px;
- background-color: var(--dark-green-50);
- }
-
- textarea {
- width: 80%;
- height: 60px;
- font-size: 14px;
- }
-
- }
-
- .cart-tax-items {
- .item-grand-total {
- font-size: 16px;
- font-weight: 700;
- color: var(--text-color);
- }
- }
-
- .column-sm-view {
- @media (max-width: 992px) {
- display: none !important;
- }
- }
-
- .item-column {
- width: 50%;
- @media (max-width: 992px) {
- width: 70%;
- }
- }
-
- .remove-cart-item {
- border-radius: 6px;
- border: 1px solid var(--gray-100);
- width: 28px;
- height: 28px;
- font-weight: 300;
- color: var(--gray-700);
- background-color: var(--gray-100);
- float: right;
- cursor: pointer;
- margin-top: .25rem;
- justify-content: center;
- }
-
- .remove-cart-item-logo {
- margin-top: 2px;
- margin-left: 2.2px;
- fill: var(--gray-700) !important;
- }
- }
-
- .cart-payment-addresses {
- hr {
- border-color: var(--border-color);
- }
- }
-
- .payment-summary {
- h6 {
- padding-bottom: 1rem;
- border-bottom: solid 1px var(--gray-200);
- }
-
- table {
- font-size: 14px;
- td {
- padding: 0;
- padding-top: 0.35rem !important;
- border: none !important;
- }
-
- &.grand-total {
- border-top: solid 1px var(--gray-200);
- }
- }
-
- .bill-label {
- color: var(--gray-600);
- }
-
- .bill-content {
- font-weight: 500;
- &.net-total {
- font-size: 16px;
- font-weight: 600;
- }
- }
-
- .btn-coupon-code {
- font-size: 14px;
- border: dashed 1px var(--gray-400);
- box-shadow: none;
- }
- }
-
- .number-spinner {
- width: 75%;
- min-width: 105px;
- .cart-btn {
- border: none;
- background: var(--gray-100);
- box-shadow: none;
- width: 24px;
- height: 28px;
- align-items: center;
- justify-content: center;
- display: flex;
- font-size: 20px;
- font-weight: 300;
- color: var(--gray-700);
- }
-
- .cart-qty {
- height: 28px;
- font-size: 13px;
- &:disabled {
- background: var(--gray-100);
- opacity: 0.65;
- }
- }
- }
-
- .place-order-container {
- .btn-place-order {
- float: right;
- }
- }
- }
-
- .t-and-c-container {
- padding: 1.5rem;
- }
-
- .t-and-c-terms {
- font-size: 14px;
- }
-}
-
-.no-image-cart-item {
- max-height: 112px;
- display: flex; justify-content: center;
- background-color: var(--gray-200);
- align-items: center;
- color: var(--gray-400);
- margin-top: .15rem;
- border-radius: 6px;
- height: 100%;
- font-size: 24px;
-}
-
-.cart-empty.frappe-card {
- min-height: 76vh;
- @include flex(flex, center, center, column);
-
- .cart-empty-message {
- font-size: 18px;
- color: var(--text-color);
- font-weight: bold;
- }
-}
-
-.address-card {
- .card-title {
- font-size: 14px;
- font-weight: 500;
- }
-
- .card-body {
- max-width: 80%;
- }
-
- .card-text {
- font-size: 13px;
- color: var(--gray-700);
- }
-
- .card-link {
- font-size: 13px;
-
- svg use {
- stroke: var(--primary-color);
- }
- }
-
- .btn-change-address {
- border: 1px solid var(--primary-color);
- color: var(--primary-color);
- box-shadow: none;
- }
-}
-
-.address-header {
- margin-top: .15rem;padding: 0;
-}
-
-.btn-new-address {
- float: right;
- font-size: 15px !important;
- color: var(--primary-color) !important;
-}
-
-.btn-new-address:hover, .btn-change-address:hover {
- color: var(--primary-color) !important;
-}
-
-.modal .address-card {
- .card-body {
- padding: var(--padding-sm);
- border-radius: var(--border-radius);
- border: 1px solid var(--dark-border-color);
- }
-}
-
-.cart-indicator {
- position: absolute;
- text-align: center;
- width: 22px;
- height: 22px;
- left: calc(100% - 40px);
- top: 22px;
-
- border-radius: 66px;
- box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
- background: white;
- color: var(--primary-color);
- font-size: 14px;
-
- &.list-indicator {
- position: unset;
- margin-left: auto;
- }
-}
-
-
-.like-action {
- visibility: hidden;
- text-align: center;
- position: absolute;
- cursor: pointer;
- width: 28px;
- height: 28px;
- left: 20px;
- top: 20px;
-
- /* White */
- background: white;
- box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
- border-radius: 66px;
-
- &.like-action-wished {
- visibility: visible !important;
- }
-
- @media (max-width: 992px) {
- visibility: visible !important;
- }
-}
-
-.like-action-list {
- visibility: hidden;
- text-align: center;
- position: absolute;
- cursor: pointer;
- width: 28px;
- height: 28px;
- left: 20px;
- top: 0;
-
- /* White */
- background: white;
- box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
- border-radius: 66px;
-
- &.like-action-wished {
- visibility: visible !important;
- }
-
- @media (max-width: 992px) {
- visibility: visible !important;
- }
-}
-
-.like-action-item-fp {
- visibility: visible !important;
- position: unset;
- float: right;
-}
-
-.like-animate {
- animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1;
-}
-
-@keyframes expand {
- 30% {
- transform: scale(1.3);
- }
- 50% {
- transform: scale(0.8);
- }
- 70% {
- transform: scale(1.1);
- }
- 100% {
- transform: scale(1);
- }
- }
-
-.not-wished {
- cursor: pointer;
- --icon-stroke: #F47A7A !important;
-
- &:hover {
- fill: #F47A7A;
- }
-}
-
-.wished {
- --icon-stroke: none;
- fill: #F47A7A !important;
-}
-
-.list-row-checkbox {
- &:before {
- display: none;
- }
-
- &:checked:before {
- display: block;
- z-index: 1;
- }
-}
-
-#pay-for-order {
- padding: .5rem 1rem; // Pay button in SO
-}
-
-.btn-explore-variants {
- visibility: hidden;
- box-shadow: none;
- margin: var(--margin-sm) 0;
- width: 90px;
- max-height: 50px; // to avoid resizing on window resize
- flex: none;
- transition: 0.3s ease;
-
- color: white;
- background-color: var(--orange-500);
- border: 1px solid var(--orange-500);
- font-size: 13px;
-
- &:hover {
- color: white;
- }
-}
-
-.btn-add-to-cart-list{
- visibility: hidden;
- box-shadow: none;
- margin: var(--margin-sm) 0;
- // margin-top: auto !important;
- max-height: 50px; // to avoid resizing on window resize
- flex: none;
- transition: 0.3s ease;
-
- font-size: 13px;
-
- &:hover {
- color: white;
- }
-
- @media (max-width: 992px) {
- visibility: visible !important;
- }
-}
-
-.go-to-cart-grid {
- max-height: 30px;
- margin-top: 1rem !important;
-}
-
-.go-to-cart {
- max-height: 30px;
- float: right;
-}
-
-.remove-wish {
- background-color: white;
- position: absolute;
- cursor: pointer;
- top:10px;
- right: 20px;
- width: 32px;
- height: 32px;
-
- border-radius: 50%;
- border: 1px solid var(--gray-100);
- box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
-}
-
-.wish-removed {
- display: none;
-}
-
-.item-website-specification {
- font-size: .875rem;
- .product-title {
- font-size: 18px;
- }
-
- .table {
- width: 70%;
- }
-
- td {
- border: none !important;
- }
-
- .spec-label {
- color: var(--gray-600);
- }
-
- .spec-content {
- color: var(--gray-800);
- }
-}
-
-.reviews-full-page {
- padding: 1rem 2rem;
-}
-
-.ratings-reviews-section {
- border-top: 1px solid #E2E6E9;
- padding: .5rem 1rem;
-}
-
-.reviews-header {
- font-size: 20px;
- font-weight: 600;
- color: var(--gray-800);
- display: flex;
- align-items: center;
- padding: 0;
-}
-
-.btn-write-review {
- float: right;
- padding: .5rem 1rem;
- font-size: 14px;
- font-weight: 400;
- border: none !important;
- box-shadow: none;
-
- color: var(--gray-900);
- background-color: var(--gray-100);
-
- &:hover {
- box-shadow: var(--btn-shadow);
- }
-}
-
-.btn-view-more {
- font-size: 14px;
-}
-
-.rating-summary-section {
- display: flex;
-}
-
-.rating-summary-title {
- margin-top: 0.15rem;
- font-size: 18px;
-}
-
-.rating-summary-numbers {
- display: flex;
- flex-direction: column;
- align-items: center;
-
- border-right: solid 1px var(--gray-100);
-}
-
-.user-review-title {
- margin-top: 0.15rem;
- font-size: 15px;
- font-weight: 600;
-}
-
-.rating {
- --star-fill: var(--gray-300);
- .star-hover {
- --star-fill: var(--yellow-100);
- }
- .star-click {
- --star-fill: var(--yellow-300);
- }
-}
-
-.ratings-pill {
- background-color: var(--gray-100);
- padding: .5rem 1rem;
- border-radius: 66px;
-}
-
-.review {
- max-width: 80%;
- line-height: 1.6;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid #E2E6E9;
-}
-
-.review-signature {
- display: flex;
- font-size: 13px;
- color: var(--gray-500);
- font-weight: 400;
-
- .reviewer {
- padding-right: 8px;
- color: var(--gray-600);
- }
-}
-
-.rating-progress-bar-section {
- padding-bottom: 2rem;
-
- .rating-bar-title {
- margin-left: -15px;
- }
-
- .rating-progress-bar {
- margin-bottom: 4px;
- height: 7px;
- margin-top: 6px;
-
- .progress-bar-cosmetic {
- background-color: var(--gray-600);
- border-radius: var(--border-radius);
- }
- }
-}
-
-.offer-container {
- font-size: 14px;
-}
-
-#search-results-container {
- border: 1px solid var(--gray-200);
- padding: .25rem 1rem;
-
- .category-chip {
- background-color: var(--gray-100);
- border: none !important;
- box-shadow: none;
- }
-
- .recent-search {
- padding: .5rem .5rem;
- border-radius: var(--border-radius);
-
- &:hover {
- background-color: var(--gray-100);
- }
- }
-}
-
-#search-box {
- background-color: white;
- height: 100%;
- padding-left: 2.5rem;
- border: 1px solid var(--gray-200);
-}
-
-.search-icon {
- position: absolute;
- left: 0;
- top: 0;
- width: 2.5rem;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- padding-bottom: 1px;
-}
-
-#toggle-view {
- float: right;
-
- .btn-primary {
- background-color: var(--gray-600);
- box-shadow: 0 0 0 0.2rem var(--gray-400);
- }
-}
-
-.placeholder-div {
- height:80%;
- width: -webkit-fill-available;
- padding: 50px;
- text-align: center;
- background-color: #F9FAFA;
- border-top-left-radius: calc(0.75rem - 1px);
- border-top-right-radius: calc(0.75rem - 1px);
-}
-.placeholder {
- font-size: 72px;
-}
-
-[data-path="cart"] {
- .modal-backdrop {
- background-color: var(--gray-50); // lighter backdrop only on cart freeze
- }
-}
-
-.item-thumb {
- height: 50px;
- max-width: 80px;
- min-width: 80px;
- object-fit: cover;
-}
-
-.brand-line {
- color: gray;
-}
-
-.btn-next, .btn-prev {
- font-size: 14px;
-}
-
-.alert-error {
- color: #e27a84;
- background-color: #fff6f7;
- border-color: #f5c6cb;
-}
-
-.font-md {
- font-size: 14px !important;
-}
-
-.in-green {
- color: var(--green-info) !important;
- font-weight: 500;
-}
-
-.has-stock {
- font-weight: 400 !important;
-}
-
-.out-of-stock {
- font-weight: 400;
- font-size: 14px;
- line-height: 20px;
- color: #F47A7A;
-}
-
-.mt-minus-2 {
- margin-top: -2rem;
-}
-
-.mt-minus-1 {
- margin-top: -1rem;
-}
\ No newline at end of file
diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py
index a910af6a1d..efeaeed324 100644
--- a/erpnext/regional/united_arab_emirates/utils.py
+++ b/erpnext/regional/united_arab_emirates/utils.py
@@ -7,32 +7,32 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax
def update_itemised_tax_data(doc):
+ # maybe this should be a standard function rather than a regional one
if not doc.taxes:
return
+ if not doc.items:
+ return
+
+ meta = frappe.get_meta(doc.items[0].doctype)
+ if not meta.has_field("tax_rate"):
+ return
+
itemised_tax = get_itemised_tax(doc.taxes)
for row in doc.items:
- tax_rate = 0.0
- item_tax_rate = 0.0
+ tax_rate, tax_amount = 0.0, 0.0
+ # dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate
+ item_code = row.item_code or row.item_name
+ if itemised_tax.get(item_code):
+ for tax in itemised_tax.get(row.item_code).values():
+ _tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate"))
+ tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount"))
+ tax_rate += _tax_rate
- if row.item_tax_rate:
- item_tax_rate = frappe.parse_json(row.item_tax_rate)
-
- # First check if tax rate is present
- # If not then look up in item_wise_tax_detail
- if item_tax_rate:
- for account, rate in item_tax_rate.items():
- tax_rate += rate
- elif row.item_code and itemised_tax.get(row.item_code):
- tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()])
-
- meta = frappe.get_meta(row.doctype)
-
- if meta.has_field("tax_rate"):
- row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
- row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
- row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
+ row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
+ row.tax_amount = flt(tax_amount, row.precision("tax_amount"))
+ row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
def get_account_currency(account):
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 8ff681b048..95d2d2c577 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -26,7 +26,6 @@ class Quotation(SellingController):
self.set_status()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
- self.validate_shopping_cart_items()
self.set_customer_name()
if self.items:
self.with_items = 1
@@ -42,26 +41,6 @@ class Quotation(SellingController):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date"))
- def validate_shopping_cart_items(self):
- if self.order_type != "Shopping Cart":
- return
-
- for item in self.items:
- has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code})
-
- # If variant is unpublished but template is published: valid
- template = frappe.get_cached_value("Item", item.item_code, "variant_of")
- if template and not has_web_item:
- has_web_item = frappe.db.exists("Website Item", {"item_code": template})
-
- if not has_web_item:
- frappe.throw(
- _("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format(
- item.idx, frappe.bold(item.item_code)
- ),
- title=_("Unpublished Item"),
- )
-
def set_has_alternative_item(self):
"""Mark 'Has Alternative Item' for rows."""
if not any(row.is_alternative for row in self.get("items")):
@@ -263,8 +242,8 @@ def make_sales_order(source_name: str, target_doc=None):
return _make_sales_order(source_name, target_doc)
-def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
- customer = _make_customer(source_name, ignore_permissions)
+def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False):
+ customer = _make_customer(source_name, ignore_permissions, customer_group)
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
@@ -428,7 +407,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
return doclist
-def _make_customer(source_name, ignore_permissions=False):
+def _make_customer(source_name, ignore_permissions=False, customer_group=None):
quotation = frappe.db.get_value(
"Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1
)
@@ -445,10 +424,7 @@ def _make_customer(source_name, ignore_permissions=False):
customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions)
customer = frappe.get_doc(customer_doclist)
customer.flags.ignore_permissions = ignore_permissions
- if quotation.get("party_name") == "Shopping Cart":
- customer.customer_group = frappe.db.get_value(
- "E Commerce Settings", None, "default_customer_group"
- )
+ customer.customer_group = customer_group
try:
customer.insert()
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 5623a12cdd..590cd3d0cf 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -161,15 +161,6 @@ class TestQuotation(FrappeTestCase):
make_sales_order(quotation.name)
- def test_shopping_cart_without_website_item(self):
- if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
- frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete()
-
- quotation = frappe.copy_doc(test_records[0])
- quotation.order_type = "Shopping Cart"
- quotation.valid_till = getdate()
- self.assertRaises(frappe.ValidationError, quotation.validate)
-
def test_create_quotation_with_margin(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
from erpnext.selling.doctype.sales_order.sales_order import (
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index ba8bc339f3..3ad18daf19 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -87,17 +87,13 @@ frappe.ui.form.on("Sales Order", {
frm.events.get_items_from_internal_purchase_order(frm);
}
- if (frm.is_new()) {
+ if (frm.doc.docstatus === 0) {
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
- if (value) {
- frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order").then((value) => {
- // If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0.
- frm.set_value("reserve_stock", value ? 1 : 0);
- })
- } else {
- // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only.
+ if (!value) {
+ // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
frm.set_value("reserve_stock", 0);
frm.set_df_property("reserve_stock", "read_only", 1);
+ frm.set_df_property("reserve_stock", "hidden", 1);
}
})
}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 084537eb4f..5b80dfd4d4 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1632,10 +1632,9 @@
{
"default": "0",
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
- "description": "If checked, Stock Reservation Entries will be created on
Submit ",
+ "description": "If checked, Stock will be reserved on
Submit ",
"fieldname": "reserve_stock",
"fieldtype": "Check",
- "hidden": 1,
"label": "Reserve Stock",
"no_copy": 1,
"print_hide": 1,
@@ -1658,7 +1657,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2023-10-10 13:36:07.526793",
+ "modified": "2023-10-18 12:41:54.813462",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 002ffe010f..eb6d63e213 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -3,6 +3,7 @@
import json
+from typing import Literal
import frappe
import frappe.utils
@@ -536,14 +537,24 @@ class SalesOrder(SellingController):
return False
@frappe.whitelist()
- def create_stock_reservation_entries(self, items_details=None, notify=True) -> None:
+ def create_stock_reservation_entries(
+ self,
+ items_details: list[dict] = None,
+ from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
+ notify=True,
+ ) -> None:
"""Creates Stock Reservation Entries for Sales Order Items."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
)
- create_stock_reservation_entries(so=self, items_details=items_details, notify=notify)
+ create_stock_reservation_entries(
+ sales_order=self,
+ items_details=items_details,
+ from_voucher_type=from_voucher_type,
+ notify=notify,
+ )
@frappe.whitelist()
def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
@@ -608,29 +619,37 @@ def close_or_unclose_sales_orders(names, status):
def get_requested_item_qty(sales_order):
- return frappe._dict(
- frappe.db.sql(
- """
- select sales_order_item, sum(qty)
- from `tabMaterial Request Item`
- where docstatus = 1
- and sales_order = %s
- group by sales_order_item
- """,
- sales_order,
- )
- )
+ result = {}
+ for d in frappe.db.get_all(
+ "Material Request Item",
+ filters={"docstatus": 1, "sales_order": sales_order},
+ fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"],
+ group_by="sales_order_item",
+ ):
+ result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty})
+
+ return result
@frappe.whitelist()
def make_material_request(source_name, target_doc=None):
requested_item_qty = get_requested_item_qty(source_name)
+ def get_remaining_qty(so_item):
+ return flt(
+ flt(so_item.qty)
+ - flt(requested_item_qty.get(so_item.name, {}).get("qty"))
+ - max(
+ flt(so_item.get("delivered_qty"))
+ - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
+ 0,
+ )
+ )
+
def update_item(source, target, source_parent):
# qty is for packed items, because packed items don't have stock_qty field
- qty = source.get("qty")
target.project = source_parent.project
- target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty"))
+ target.qty = get_remaining_qty(source)
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
args = target.as_dict().copy()
@@ -663,8 +682,8 @@ def make_material_request(source_name, target_doc=None):
"Sales Order Item": {
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
- "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
- and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
+ "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
+ and get_remaining_qty(item) > 0,
"postprocess": update_item,
},
},
@@ -814,6 +833,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
"postprocess": update_dn_item,
}
},
+ ignore_permissions=True,
)
dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1))
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 83689a2b0b..d8b5878aa3 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1784,10 +1784,10 @@ class TestSalesOrder(FrappeTestCase):
si.submit()
pe.load_from_db()
- self.assertEqual(pe.references[0].reference_name, si.name)
- self.assertEqual(pe.references[0].allocated_amount, 200)
- self.assertEqual(pe.references[1].reference_name, so.name)
- self.assertEqual(pe.references[1].allocated_amount, 300)
+ self.assertEqual(pe.references[0].reference_name, so.name)
+ self.assertEqual(pe.references[0].allocated_amount, 300)
+ self.assertEqual(pe.references[1].reference_name, si.name)
+ self.assertEqual(pe.references[1].allocated_amount, 200)
def test_delivered_item_material_request(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index e6f7456620..f82047f511 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -68,6 +68,7 @@
"total_weight",
"column_break_21",
"weight_uom",
+ "accounting_dimensions_section",
"warehouse_and_reference",
"warehouse",
"target_warehouse",
@@ -889,12 +890,18 @@
"label": "Production Plan Qty",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-07-28 14:56:42.031636",
+ "modified": "2023-10-17 18:18:26.475259",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
@@ -905,4 +912,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 6855012d5f..d6829ce24b 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -25,7 +25,7 @@
"so_required",
"dn_required",
"sales_update_frequency",
- "over_order_allowance",
+ "blanket_order_allowance",
"column_break_5",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
@@ -183,12 +183,6 @@
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
},
- {
- "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
- "fieldname": "over_order_allowance",
- "fieldtype": "Float",
- "label": "Over Order Allowance (%)"
- },
{
"default": "0",
"fieldname": "dont_reserve_sales_order_qty_on_sales_return",
@@ -200,6 +194,12 @@
"fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check",
"label": "Allow Negative rates for Items"
+ },
+ {
+ "description": "Percentage you are allowed to sell beyond the Blanket Order quantity.",
+ "fieldname": "blanket_order_allowance",
+ "fieldtype": "Float",
+ "label": "Blanket Order Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -207,7 +207,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-08-14 20:33:05.693667",
+ "modified": "2023-10-25 14:03:03.966701",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 4973dab505..23b93dc161 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -221,7 +221,6 @@ erpnext.company.setup_queries = function(frm) {
["cost_center", {}],
["round_off_cost_center", {}],
["depreciation_cost_center", {}],
- ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}],
@@ -236,8 +235,6 @@ erpnext.company.setup_queries = function(frm) {
$.each([
["stock_adjustment_account",
{"root_type": "Expense", "account_type": "Stock Adjustment"}],
- ["expenses_included_in_valuation",
- {"root_type": "Expense", "account_type": "Expenses Included in Valuation"}],
["stock_received_but_not_billed",
{"root_type": "Liability", "account_type": "Stock Received But Not Billed"}],
["service_received_but_not_billed",
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 24d7da45b8..b9ff3dddd1 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -80,7 +80,6 @@
"accumulated_depreciation_account",
"depreciation_expense_account",
"series_for_depreciation_entry",
- "expenses_included_in_asset_valuation",
"column_break_40",
"disposal_account",
"depreciation_cost_center",
@@ -103,11 +102,10 @@
"enable_provisional_accounting_for_non_stock_items",
"default_inventory_account",
"stock_adjustment_account",
- "default_in_transit_warehouse",
"column_break_32",
"stock_received_but_not_billed",
"default_provisional_account",
- "expenses_included_in_valuation",
+ "default_in_transit_warehouse",
"dashboard_tab"
],
"fields": [
@@ -469,14 +467,6 @@
"no_copy": 1,
"options": "Account"
},
- {
- "fieldname": "expenses_included_in_valuation",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Expenses Included In Valuation",
- "no_copy": 1,
- "options": "Account"
- },
{
"fieldname": "accumulated_depreciation_account",
"fieldtype": "Link",
@@ -496,12 +486,6 @@
"fieldtype": "Data",
"label": "Series for Asset Depreciation Entry (Journal Entry)"
},
- {
- "fieldname": "expenses_included_in_asset_valuation",
- "fieldtype": "Link",
- "label": "Expenses Included In Asset Valuation",
- "options": "Account"
- },
{
"fieldname": "column_break_40",
"fieldtype": "Column Break"
@@ -782,7 +766,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2023-09-10 21:53:13.860791",
+ "modified": "2023-10-23 10:19:24.322898",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index b05696ad96..3413702c5a 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -92,7 +92,6 @@ class Company(NestedSet):
["Default Income Account", "default_income_account"],
["Stock Received But Not Billed Account", "stock_received_but_not_billed"],
["Stock Adjustment Account", "stock_adjustment_account"],
- ["Expense Included In Valuation Account", "expenses_included_in_valuation"],
]
for account in accounts:
@@ -384,7 +383,6 @@ class Company(NestedSet):
"depreciation_expense_account": "Depreciation",
"capital_work_in_progress_account": "Capital Work in Progress",
"asset_received_but_not_billed": "Asset Received But Not Billed",
- "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation",
"default_expense_account": "Cost of Goods Sold",
}
@@ -394,7 +392,6 @@ class Company(NestedSet):
"stock_received_but_not_billed": "Stock Received But Not Billed",
"default_inventory_account": "Stock",
"stock_adjustment_account": "Stock Adjustment",
- "expenses_included_in_valuation": "Expenses Included In Valuation",
}
)
diff --git a/erpnext/setup/doctype/driver/driver.json b/erpnext/setup/doctype/driver/driver.json
index 8d426cc29a..2e994b5ff9 100644
--- a/erpnext/setup/doctype/driver/driver.json
+++ b/erpnext/setup/doctype/driver/driver.json
@@ -157,6 +157,22 @@
"role": "HR Manager",
"share": 1,
"write": 1
+ },
+ {
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery Manager",
+ "share": 1,
+ "write": 1
}
],
"quick_entry": 1,
@@ -166,4 +182,4 @@
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py
index 566392c327..78fb4dfc58 100755
--- a/erpnext/setup/doctype/employee/employee.py
+++ b/erpnext/setup/doctype/employee/employee.py
@@ -123,7 +123,7 @@ class Employee(NestedSet):
user.gender = self.gender
if self.image:
- if not user.user_image:
+ if not user.user_image or self.has_value_changed("image"):
user.user_image = self.image
try:
frappe.get_doc(
diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js
index 4b04ac1d5e..d6eb11f73d 100644
--- a/erpnext/setup/doctype/item_group/item_group.js
+++ b/erpnext/setup/doctype/item_group/item_group.js
@@ -71,20 +71,6 @@ frappe.ui.form.on("Item Group", {
frappe.set_route("List", "Item", {"item_group": frm.doc.name});
});
}
-
- frappe.model.with_doctype('Website Item', () => {
- const web_item_meta = frappe.get_meta('Website Item');
-
- const valid_fields = web_item_meta.fields.filter(df =>
- ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
- ).map(df =>
- ({ label: df.label, value: df.fieldname })
- );
-
- frm.get_field("filter_fields").grid.update_docfield_property(
- 'fieldname', 'options', valid_fields
- );
- });
},
set_root_readonly: function(frm) {
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index e0f5090474..dfa5a8ed0a 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -19,22 +19,9 @@
"item_group_defaults",
"sec_break_taxes",
"taxes",
- "sb9",
- "route",
- "website_title",
- "description",
- "show_in_website",
- "include_descendants",
- "column_break_16",
- "weightage",
- "slideshow",
- "website_specifications",
- "website_filters_section",
- "filter_fields",
- "filter_attributes",
"lft",
- "rgt",
- "old_parent"
+ "old_parent",
+ "rgt"
],
"fields": [
{
@@ -106,54 +93,6 @@
"label": "Taxes",
"options": "Item Tax"
},
- {
- "fieldname": "sb9",
- "fieldtype": "Section Break",
- "label": "Website Settings"
- },
- {
- "default": "0",
- "description": "Make Item Group visible in website",
- "fieldname": "show_in_website",
- "fieldtype": "Check",
- "label": "Show in Website"
- },
- {
- "depends_on": "show_in_website",
- "fieldname": "route",
- "fieldtype": "Data",
- "label": "Route",
- "no_copy": 1,
- "unique": 1
- },
- {
- "depends_on": "show_in_website",
- "fieldname": "weightage",
- "fieldtype": "Int",
- "label": "Weightage"
- },
- {
- "depends_on": "show_in_website",
- "description": "Show this slideshow at the top of the page",
- "fieldname": "slideshow",
- "fieldtype": "Link",
- "label": "Slideshow",
- "options": "Website Slideshow"
- },
- {
- "depends_on": "show_in_website",
- "description": "HTML / Banner that will show on the top of product list.",
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "label": "Description"
- },
- {
- "depends_on": "show_in_website",
- "fieldname": "website_specifications",
- "fieldtype": "Table",
- "label": "Website Specifications",
- "options": "Item Website Specification"
- },
{
"fieldname": "lft",
"fieldtype": "Int",
@@ -188,43 +127,6 @@
"options": "Item Group",
"print_hide": 1,
"report_hide": 1
- },
- {
- "collapsible": 1,
- "depends_on": "show_in_website",
- "fieldname": "website_filters_section",
- "fieldtype": "Section Break",
- "label": "Website Filters"
- },
- {
- "fieldname": "filter_fields",
- "fieldtype": "Table",
- "label": "Item Fields",
- "options": "Website Filter Field"
- },
- {
- "fieldname": "filter_attributes",
- "fieldtype": "Table",
- "label": "Attributes",
- "options": "Website Attribute"
- },
- {
- "depends_on": "show_in_website",
- "fieldname": "website_title",
- "fieldtype": "Data",
- "label": "Title"
- },
- {
- "fieldname": "column_break_16",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "depends_on": "show_in_website",
- "description": "Include Website Items belonging to child Item Groups",
- "fieldname": "include_descendants",
- "fieldtype": "Check",
- "label": "Include Descendants"
}
],
"icon": "fa fa-sitemap",
@@ -233,7 +135,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2023-08-28 22:27:48.382985",
+ "modified": "2023-10-12 13:44:13.611287",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index cc67c696b4..fe7a241dc4 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -2,39 +2,19 @@
# License: GNU General Public License v3. See license.txt
import copy
-from urllib.parse import quote
import frappe
from frappe import _
-from frappe.utils import cint
from frappe.utils.nestedset import NestedSet
-from frappe.website.utils import clear_cache
-from frappe.website.website_generator import WebsiteGenerator
-
-from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings
-from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
-class ItemGroup(NestedSet, WebsiteGenerator):
- nsm_parent_field = "parent_item_group"
- website = frappe._dict(
- condition_field="show_in_website",
- template="templates/generators/item_group.html",
- no_cache=1,
- no_breadcrumbs=1,
- )
-
+class ItemGroup(NestedSet):
def validate(self):
- super(ItemGroup, self).validate()
-
if not self.parent_item_group and not frappe.flags.in_test:
if frappe.db.exists("Item Group", _("All Item Groups")):
self.parent_item_group = _("All Item Groups")
-
- self.make_route()
self.validate_item_group_defaults()
self.check_item_tax()
- ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
def check_item_tax(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
@@ -53,66 +33,13 @@ class ItemGroup(NestedSet, WebsiteGenerator):
def on_update(self):
NestedSet.on_update(self)
- invalidate_cache_for(self)
self.validate_one_root()
self.delete_child_item_groups_key()
- def make_route(self):
- """Make website route"""
- if not self.route:
- self.route = ""
- if self.parent_item_group:
- parent_item_group = frappe.get_doc("Item Group", self.parent_item_group)
-
- # make parent route only if not root
- if parent_item_group.parent_item_group and parent_item_group.route:
- self.route = parent_item_group.route + "/"
-
- self.route += self.scrub(self.item_group_name)
-
- return self.route
-
def on_trash(self):
NestedSet.on_trash(self, allow_root_deletion=True)
- WebsiteGenerator.on_trash(self)
self.delete_child_item_groups_key()
- def get_context(self, context):
- context.show_search = True
- context.body_class = "product-page"
- context.page_length = (
- cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 6
- )
- context.search_link = "/product_search"
-
- filter_engine = ProductFiltersBuilder(self.name)
-
- context.field_filters = filter_engine.get_field_filters()
- context.attribute_filters = filter_engine.get_attribute_filters()
-
- context.update({"parents": get_parent_item_groups(self.parent_item_group), "title": self.name})
-
- if self.slideshow:
- values = {"show_indicators": 1, "show_controls": 0, "rounded": 1, "slider_name": self.slideshow}
- slideshow = frappe.get_doc("Website Slideshow", self.slideshow)
- slides = slideshow.get({"doctype": "Website Slideshow Item"})
- for index, slide in enumerate(slides):
- values[f"slide_{index + 1}_image"] = slide.image
- values[f"slide_{index + 1}_title"] = slide.heading
- values[f"slide_{index + 1}_subtitle"] = slide.description
- values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light"
- values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre"
- values[f"slide_{index + 1}_primary_action"] = slide.url
-
- context.slideshow = values
-
- context.no_breadcrumbs = False
- context.title = self.website_title or self.name
- context.name = self.name
- context.item_group_name = self.item_group_name
-
- return context
-
def delete_child_item_groups_key(self):
frappe.cache().hdel("child_item_groups", self.name)
@@ -122,20 +49,6 @@ class ItemGroup(NestedSet, WebsiteGenerator):
validate_item_default_company_links(self.item_group_defaults)
-def get_child_groups_for_website(item_group_name, immediate=False, include_self=False):
- """Returns child item groups *excluding* passed group."""
- item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
- filters = {"lft": [">", item_group.lft], "rgt": ["<", item_group.rgt], "show_in_website": 1}
-
- if immediate:
- filters["parent_item_group"] = item_group_name
-
- if include_self:
- filters.update({"lft": [">=", item_group.lft], "rgt": ["<=", item_group.rgt]})
-
- return frappe.get_all("Item Group", filters=filters, fields=["name", "route"], order_by="name")
-
-
def get_child_item_groups(item_group_name):
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
@@ -149,63 +62,6 @@ def get_child_item_groups(item_group_name):
return child_item_groups or {}
-def get_item_for_list_in_html(context):
- # add missing absolute link in files
- # user may forget it during upload
- if (context.get("website_image") or "").startswith("files/"):
- context["website_image"] = "/" + quote(context["website_image"])
-
- products_template = "templates/includes/products_as_list.html"
-
- return frappe.get_template(products_template).render(context)
-
-
-def get_parent_item_groups(item_group_name, from_item=False):
- settings = frappe.get_cached_doc("E Commerce Settings")
-
- if settings.enable_field_filters:
- base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"}
- else:
- base_nav_page = {"name": _("All Products"), "route": "/all-products"}
-
- if from_item and frappe.request.environ.get("HTTP_REFERER"):
- # base page after 'Home' will vary on Item page
- last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
- if last_page and last_page in ("shop-by-category", "all-products"):
- base_nav_page_title = " ".join(last_page.split("-")).title()
- base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
-
- base_parents = [
- {"name": _("Home"), "route": "/"},
- base_nav_page,
- ]
-
- if not item_group_name:
- return base_parents
-
- item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
- parent_groups = frappe.db.sql(
- """select name, route from `tabItem Group`
- where lft <= %s and rgt >= %s
- and show_in_website=1
- order by lft asc""",
- (item_group.lft, item_group.rgt),
- as_dict=True,
- )
-
- return base_parents + parent_groups
-
-
-def invalidate_cache_for(doc, item_group=None):
- if not item_group:
- item_group = doc.name
-
- for d in get_parent_item_groups(item_group):
- item_group_name = frappe.db.get_value("Item Group", d.get("name"))
- if item_group_name:
- clear_cache(frappe.db.get_value("Item Group", item_group_name, "route"))
-
-
def get_item_group_defaults(item, company):
item = frappe.get_cached_doc("Item", item)
item_group = frappe.get_cached_doc("Item Group", item.item_group)
diff --git a/erpnext/setup/doctype/vehicle/vehicle.json b/erpnext/setup/doctype/vehicle/vehicle.json
index ed803a763a..b19d45924f 100644
--- a/erpnext/setup/doctype/vehicle/vehicle.json
+++ b/erpnext/setup/doctype/vehicle/vehicle.json
@@ -860,6 +860,22 @@
"share": 1,
"submit": 0,
"write": 1
+ },
+ {
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery Manager",
+ "share": 1,
+ "write": 1
}
],
"quick_entry": 1,
@@ -872,4 +888,4 @@
"title_field": "",
"track_changes": 1,
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 85eaf5fa92..b106cfcc1a 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -33,6 +33,7 @@ def after_install():
add_app_name()
setup_log_settings()
hide_workspaces()
+ update_roles()
frappe.db.commit()
@@ -232,6 +233,12 @@ def hide_workspaces():
frappe.db.set_value("Workspace", ws, "public", 0)
+def update_roles():
+ website_user_roles = ("Customer", "Supplier")
+ for role in website_user_roles:
+ frappe.db.set_value("Role", role, "desk_access", 0)
+
+
def create_default_role_profiles():
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
role_profile = frappe.new_doc("Role Profile")
diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py
index ace5cca0b0..d4aac5ee46 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -33,20 +33,6 @@ def create_fiscal_year_and_company(args):
).insert()
-def enable_shopping_cart(args): # nosemgrep
- # Needs price_lists
- frappe.get_doc(
- {
- "doctype": "E Commerce Settings",
- "enabled": 1,
- "company": args.get("company_name"),
- "price_list": frappe.db.get_value("Price List", {"selling": 1}),
- "default_customer_group": _("Individual"),
- "quotation_series": "QTN-",
- }
- ).insert()
-
-
def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year:
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index ae6881b99e..2205924e50 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -454,7 +454,6 @@ def install_defaults(args=None): # nosemgrep
set_global_defaults(args)
update_stock_settings()
- update_shopping_cart_settings(args)
args.update({"set_default": 1})
create_bank_account(args)
@@ -529,20 +528,6 @@ def create_bank_account(args):
pass
-def update_shopping_cart_settings(args): # nosemgrep
- shopping_cart = frappe.get_doc("E Commerce Settings")
- shopping_cart.update(
- {
- "enabled": 1,
- "company": args.company_name,
- "price_list": frappe.db.get_value("Price List", {"selling": 1}),
- "default_customer_group": _("Individual"),
- "quotation_series": "QTN-",
- }
- )
- shopping_cart.update_single(shopping_cart.get_valid_dict())
-
-
def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year:
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 5806fd1f78..2f9cec40b0 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -1,500 +1,500 @@
{
- "charts": [],
- "content": "[{\"id\":\"NO5yYHJopc\",\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t \",\"col\":12}},{\"id\":\"CDxIM-WuZ9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"-Uh7DKJNJX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"id\":\"K9ST9xcDXh\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"id\":\"27IdVHVQMb\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"id\":\"Rwp5zff88b\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"id\":\"hkfnQ2sevf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Global Defaults\",\"col\":3}},{\"id\":\"jjxI_PDawD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"R3CoYYFXye\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yynbm1J_VO\",\"type\":\"header\",\"data\":{\"text\":\"
Settings \",\"col\":12}},{\"id\":\"KDCv2MvSg3\",\"type\":\"card\",\"data\":{\"card_name\":\"Module Settings\",\"col\":4}},{\"id\":\"Q0_bqT7cxQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"UnqK5haBnh\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"kp7u1H5hCd\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"Ufc3jycgy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"89bSNzv3Yh\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
- "creation": "2022-01-27 13:14:47.349433",
- "custom_blocks": [],
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "setting",
- "idx": 0,
- "is_hidden": 0,
- "label": "ERPNext Settings",
- "links": [
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Import Data",
- "link_count": 0,
- "link_to": "Data Import",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Export Data",
- "link_count": 0,
- "link_to": "Data Export",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Bulk Update",
- "link_count": 0,
- "link_to": "Bulk Update",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Download Backups",
- "link_count": 0,
- "link_to": "backups",
- "link_type": "Page",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Deleted Documents",
- "link_count": 0,
- "link_to": "Deleted Document",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email / Notifications",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Account",
- "link_count": 0,
- "link_to": "Email Account",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Domain",
- "link_count": 0,
- "link_to": "Email Domain",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Notification",
- "link_count": 0,
- "link_to": "Notification",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Template",
- "link_count": 0,
- "link_to": "Email Template",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Auto Email Report",
- "link_count": 0,
- "link_to": "Auto Email Report",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Newsletter",
- "link_count": 0,
- "link_to": "Newsletter",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Notification Settings",
- "link_count": 0,
- "link_to": "Notification Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Website",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Website Settings",
- "link_count": 0,
- "link_to": "Website Settings",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Website Theme",
- "link_count": 0,
- "link_to": "Website Theme",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Website Script",
- "link_count": 0,
- "link_to": "Website Script",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "About Us Settings",
- "link_count": 0,
- "link_to": "About Us Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Contact Us Settings",
- "link_count": 0,
- "link_to": "Contact Us Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Printing",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Format Builder",
- "link_count": 0,
- "link_to": "print-format-builder",
- "link_type": "Page",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Settings",
- "link_count": 0,
- "link_to": "Print Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Format",
- "link_count": 0,
- "link_to": "Print Format",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Style",
- "link_count": 0,
- "link_to": "Print Style",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workflow",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workflow",
- "link_count": 0,
- "link_to": "Workflow",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workflow State",
- "link_count": 0,
- "link_to": "Workflow State",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workflow Action",
- "link_count": 0,
- "link_to": "Workflow Action",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Core",
- "link_count": 3,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "System Settings",
- "link_count": 0,
- "link_to": "System Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Domain Settings",
- "link_count": 0,
- "link_to": "Domain Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Global Defaults",
- "link_count": 0,
- "link_to": "Global Defaults",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Module Settings",
- "link_count": 8,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Accounts Settings",
- "link_count": 0,
- "link_to": "Accounts Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Stock Settings",
- "link_count": 0,
- "link_to": "Stock Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Selling Settings",
- "link_count": 0,
- "link_to": "Selling Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Buying Settings",
- "link_count": 0,
- "link_to": "Buying Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Manufacturing Settings",
- "link_count": 0,
- "link_to": "Manufacturing Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "CRM Settings",
- "link_count": 0,
- "link_to": "CRM Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Projects Settings",
- "link_count": 0,
- "link_to": "Projects Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Support Settings",
- "link_count": 0,
- "link_to": "Support Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- }
- ],
- "modified": "2023-05-24 14:47:25.356531",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "ERPNext Settings",
- "number_cards": [],
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "quick_lists": [],
- "restrict_to_domain": "",
- "roles": [],
- "sequence_id": 19.0,
- "shortcuts": [
- {
- "color": "Grey",
- "doc_view": "List",
- "label": "Print Settings",
- "link_to": "Print Settings",
- "type": "DocType"
- },
- {
- "color": "Grey",
- "doc_view": "List",
- "label": "System Settings",
- "link_to": "System Settings",
- "type": "DocType"
- },
- {
- "icon": "accounting",
- "label": "Accounts Settings",
- "link_to": "Accounts Settings",
- "type": "DocType"
- },
- {
- "color": "Grey",
- "doc_view": "List",
- "label": "Global Defaults",
- "link_to": "Global Defaults",
- "type": "DocType"
- },
- {
- "icon": "stock",
- "label": "Stock Settings",
- "link_to": "Stock Settings",
- "type": "DocType"
- },
- {
- "icon": "sell",
- "label": "Selling Settings",
- "link_to": "Selling Settings",
- "type": "DocType"
- },
- {
- "icon": "buying",
- "label": "Buying Settings",
- "link_to": "Buying Settings",
- "type": "DocType"
- }
- ],
- "title": "ERPNext Settings"
-}
\ No newline at end of file
+ "charts": [],
+ "content": "[{\"id\":\"NO5yYHJopc\",\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t \",\"col\":12}},{\"id\":\"CDxIM-WuZ9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"-Uh7DKJNJX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"id\":\"K9ST9xcDXh\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"id\":\"27IdVHVQMb\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"id\":\"Rwp5zff88b\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"id\":\"hkfnQ2sevf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Global Defaults\",\"col\":3}},{\"id\":\"jjxI_PDawD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"R3CoYYFXye\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yynbm1J_VO\",\"type\":\"header\",\"data\":{\"text\":\"
Settings \",\"col\":12}},{\"id\":\"KDCv2MvSg3\",\"type\":\"card\",\"data\":{\"card_name\":\"Module Settings\",\"col\":4}},{\"id\":\"Q0_bqT7cxQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"UnqK5haBnh\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"kp7u1H5hCd\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"Ufc3jycgy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"89bSNzv3Yh\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
+ "creation": "2022-01-27 13:14:47.349433",
+ "custom_blocks": [],
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "for_user": "",
+ "hide_custom": 0,
+ "icon": "setting",
+ "idx": 0,
+ "is_hidden": 0,
+ "label": "ERPNext Settings",
+ "links": [
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Import Data",
+ "link_count": 0,
+ "link_to": "Data Import",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Export Data",
+ "link_count": 0,
+ "link_to": "Data Export",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bulk Update",
+ "link_count": 0,
+ "link_to": "Bulk Update",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Download Backups",
+ "link_count": 0,
+ "link_to": "backups",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Deleted Documents",
+ "link_count": 0,
+ "link_to": "Deleted Document",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email / Notifications",
+ "link_count": 0,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Account",
+ "link_count": 0,
+ "link_to": "Email Account",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Domain",
+ "link_count": 0,
+ "link_to": "Email Domain",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Notification",
+ "link_count": 0,
+ "link_to": "Notification",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Template",
+ "link_count": 0,
+ "link_to": "Email Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Auto Email Report",
+ "link_count": 0,
+ "link_to": "Auto Email Report",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Newsletter",
+ "link_count": 0,
+ "link_to": "Newsletter",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Notification Settings",
+ "link_count": 0,
+ "link_to": "Notification Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Website",
+ "link_count": 0,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Website Settings",
+ "link_count": 0,
+ "link_to": "Website Settings",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Website Theme",
+ "link_count": 0,
+ "link_to": "Website Theme",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Website Script",
+ "link_count": 0,
+ "link_to": "Website Script",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "About Us Settings",
+ "link_count": 0,
+ "link_to": "About Us Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Contact Us Settings",
+ "link_count": 0,
+ "link_to": "Contact Us Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Printing",
+ "link_count": 0,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Print Format Builder",
+ "link_count": 0,
+ "link_to": "print-format-builder",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Print Settings",
+ "link_count": 0,
+ "link_to": "Print Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Print Format",
+ "link_count": 0,
+ "link_to": "Print Format",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Print Style",
+ "link_count": 0,
+ "link_to": "Print Style",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workflow",
+ "link_count": 0,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workflow",
+ "link_count": 0,
+ "link_to": "Workflow",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workflow State",
+ "link_count": 0,
+ "link_to": "Workflow State",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workflow Action",
+ "link_count": 0,
+ "link_to": "Workflow Action",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Core",
+ "link_count": 3,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "System Settings",
+ "link_count": 0,
+ "link_to": "System Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Domain Settings",
+ "link_count": 0,
+ "link_to": "Domain Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Global Defaults",
+ "link_count": 0,
+ "link_to": "Global Defaults",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Module Settings",
+ "link_count": 8,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounts Settings",
+ "link_count": 0,
+ "link_to": "Accounts Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Settings",
+ "link_count": 0,
+ "link_to": "Stock Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Selling Settings",
+ "link_count": 0,
+ "link_to": "Selling Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Buying Settings",
+ "link_count": 0,
+ "link_to": "Buying Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Manufacturing Settings",
+ "link_count": 0,
+ "link_to": "Manufacturing Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "CRM Settings",
+ "link_count": 0,
+ "link_to": "CRM Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Projects Settings",
+ "link_count": 0,
+ "link_to": "Projects Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Support Settings",
+ "link_count": 0,
+ "link_to": "Support Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2023-05-24 14:47:25.356531",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "ERPNext Settings",
+ "number_cards": [],
+ "owner": "Administrator",
+ "parent_page": "",
+ "public": 1,
+ "quick_lists": [],
+ "restrict_to_domain": "",
+ "roles": [],
+ "sequence_id": 19.0,
+ "shortcuts": [
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Print Settings",
+ "link_to": "Print Settings",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "System Settings",
+ "link_to": "System Settings",
+ "type": "DocType"
+ },
+ {
+ "icon": "accounting",
+ "label": "Accounts Settings",
+ "link_to": "Accounts Settings",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Global Defaults",
+ "link_to": "Global Defaults",
+ "type": "DocType"
+ },
+ {
+ "icon": "stock",
+ "label": "Stock Settings",
+ "link_to": "Stock Settings",
+ "type": "DocType"
+ },
+ {
+ "icon": "sell",
+ "label": "Selling Settings",
+ "link_to": "Selling Settings",
+ "type": "DocType"
+ },
+ {
+ "icon": "buying",
+ "label": "Buying Settings",
+ "link_to": "Buying Settings",
+ "type": "DocType"
+ }
+ ],
+ "title": "ERPNext Settings"
+ }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
index 295d979b83..b0499bfe86 100644
--- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
+++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
@@ -1,6 +1,6 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
+import gzip
import json
import frappe
@@ -8,7 +8,7 @@ from frappe import _
from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file
from frappe.desk.form.load import get_attachments
from frappe.model.document import Document
-from frappe.utils import get_link_to_form, gzip_decompress, parse_json
+from frappe.utils import get_link_to_form, parse_json
from frappe.utils.background_jobs import enqueue
from erpnext.stock.report.stock_balance.stock_balance import execute
@@ -109,7 +109,7 @@ class ClosingStockBalance(Document):
attachment = attachments[0]
attached_file = frappe.get_doc("File", attachment.name)
- data = gzip_decompress(attached_file.get_content())
+ data = gzip.decompress(attached_file.get_content())
if data := json.loads(data.decode("utf-8")):
data = data
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index e0d49192eb..b85f296d0b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1460,6 +1460,36 @@
"read": 1,
"role": "Stock Manager",
"write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
}
],
"search_fields": "status,customer,customer_name, territory,base_grand_total",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 48b8ab7504..1eecf6dc2a 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1230,6 +1230,21 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.rollback()
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
+ def test_non_internal_transfer_delivery_note(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ dn = create_delivery_note(do_not_submit=True)
+ warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company)
+ dn.items[0].db_set("target_warehouse", warehouse)
+
+ dn.reload()
+
+ self.assertEqual(dn.items[0].target_warehouse, warehouse)
+
+ dn.save()
+ dn.reload()
+ self.assertFalse(dn.items[0].target_warehouse)
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 612d674e01..6148950462 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -725,7 +725,8 @@
"label": "Against Delivery Note Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "stock_qty_sec_break",
@@ -892,7 +893,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-07-26 12:53:49.357171",
+ "modified": "2023-10-16 16:18:18.013379",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
@@ -902,4 +903,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_settings/delivery_settings.json b/erpnext/stock/doctype/delivery_settings/delivery_settings.json
index 963403b8f2..ad0ac45851 100644
--- a/erpnext/stock/doctype/delivery_settings/delivery_settings.json
+++ b/erpnext/stock/doctype/delivery_settings/delivery_settings.json
@@ -239,7 +239,7 @@
"print": 1,
"read": 1,
"report": 0,
- "role": "System Manager",
+ "role": "Delivery Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
@@ -255,4 +255,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/delivery_stop/delivery_stop.json b/erpnext/stock/doctype/delivery_stop/delivery_stop.json
index 5610a8108a..42560e612e 100644
--- a/erpnext/stock/doctype/delivery_stop/delivery_stop.json
+++ b/erpnext/stock/doctype/delivery_stop/delivery_stop.json
@@ -1,815 +1,197 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-10-16 16:46:28.166950",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-10-16 16:46:28.166950",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "customer",
+ "address",
+ "locked",
+ "column_break_6",
+ "customer_address",
+ "visited",
+ "order_information_section",
+ "delivery_note",
+ "cb_order",
+ "grand_total",
+ "section_break_7",
+ "contact",
+ "email_sent_to",
+ "column_break_7",
+ "customer_contact",
+ "section_break_9",
+ "distance",
+ "estimated_arrival",
+ "lat",
+ "column_break_19",
+ "uom",
+ "lng",
+ "more_information_section",
+ "details"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "customer",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "options": "Customer",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Customer",
+ "options": "Customer"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Address Name",
- "length": 0,
- "no_copy": 0,
- "options": "Address",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "address",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Address Name",
+ "options": "Address",
+ "print_hide": 1,
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "lock",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Lock",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "locked",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Locked"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_6",
- "fieldtype": "Column Break",
- "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,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer_address",
- "fieldtype": "Small Text",
- "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": "Customer Address",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "customer_address",
+ "fieldtype": "Small Text",
+ "label": "Customer Address",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.docstatus==1",
- "fieldname": "visited",
- "fieldtype": "Check",
- "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": "Visited",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "default": "0",
+ "depends_on": "eval:doc.docstatus==1",
+ "fieldname": "visited",
+ "fieldtype": "Check",
+ "label": "Visited",
+ "no_copy": 1,
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "order_information_section",
- "fieldtype": "Section Break",
- "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": "Order Information",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "order_information_section",
+ "fieldtype": "Section Break",
+ "label": "Order Information"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "delivery_note",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Delivery Note",
- "length": 0,
- "no_copy": 1,
- "options": "Delivery Note",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "delivery_note",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Delivery Note",
+ "no_copy": 1,
+ "options": "Delivery Note",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "cb_order",
- "fieldtype": "Column Break",
- "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,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "cb_order",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "grand_total",
- "fieldtype": "Currency",
- "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": "Grand Total",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "grand_total",
+ "fieldtype": "Currency",
+ "label": "Grand Total",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "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": "Contact Information",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
+ "label": "Contact Information"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact",
- "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": "Contact Name",
- "length": 0,
- "no_copy": 0,
- "options": "Contact",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "contact",
+ "fieldtype": "Link",
+ "label": "Contact Name",
+ "options": "Contact",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email_sent_to",
- "fieldtype": "Data",
- "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": "Email sent to",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "email_sent_to",
+ "fieldtype": "Data",
+ "label": "Email sent to",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "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,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer_contact",
- "fieldtype": "Small Text",
- "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": "Customer Contact",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "customer_contact",
+ "fieldtype": "Small Text",
+ "label": "Customer Contact",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "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": "Dispatch Information",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "label": "Dispatch Information"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "distance",
- "fieldtype": "Float",
- "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": "Distance",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "distance",
+ "fieldtype": "Float",
+ "label": "Distance",
+ "precision": "2",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "estimated_arrival",
- "fieldtype": "Datetime",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Estimated Arrival",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "estimated_arrival",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Estimated Arrival"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "lat",
- "fieldtype": "Float",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Latitude",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "lat",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Latitude"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_19",
- "fieldtype": "Column Break",
- "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,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "depends_on": "eval:doc.distance",
- "fieldname": "uom",
- "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": "UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.distance",
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "lng",
- "fieldtype": "Float",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Longitude",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "lng",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Longitude"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "more_information_section",
- "fieldtype": "Section Break",
- "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": "More Information",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "more_information_section",
+ "fieldtype": "Section Break",
+ "label": "More Information"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "details",
- "fieldtype": "Text Editor",
- "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": "Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "details",
+ "fieldtype": "Text Editor",
+ "label": "Details"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-10-16 05:23:25.661542",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Delivery Stop",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-09-29 09:22:53.435161",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Delivery Stop",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
index de503dc73f..158bd0cac1 100755
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
@@ -62,8 +62,13 @@ frappe.ui.form.on('Delivery Trip', {
company: frm.doc.company,
}
})
- }, __("Get customers from"));
+ }, __("Get stops from"));
}
+ frm.add_custom_button(__("Delivery Notes"), function () {
+ frappe.set_route("List", "Delivery Note",
+ {'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]}
+ );
+ }, __("View"));
},
calculate_arrival_time: function (frm) {
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
index 9d8fe46e8c..ec72af8404 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
@@ -188,7 +188,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2023-06-27 11:22:27.927637",
+ "modified": "2023-10-01 07:06:06.314503",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Trip",
@@ -224,10 +224,40 @@
"share": 1,
"submit": 1,
"write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Delivery Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "driver_name"
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index af2f4113e1..c531a8769c 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -170,7 +170,7 @@ class DeliveryTrip(Document):
for stop in self.delivery_stops:
leg.append(stop.customer_address)
- if optimize and stop.lock:
+ if optimize and stop.locked:
route_list.append(leg)
leg = [stop.customer_address]
diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
index ed699e37b8..9b8b46e6e0 100644
--- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
@@ -46,7 +46,7 @@ class TestDeliveryTrip(FrappeTestCase):
self.assertEqual(len(route_list[0]), 4)
def test_unoptimized_route_list_with_locks(self):
- self.delivery_trip.delivery_stops[0].lock = 1
+ self.delivery_trip.delivery_stops[0].locked = 1
self.delivery_trip.save()
route_list = self.delivery_trip.form_route_list(optimize=False)
@@ -65,7 +65,7 @@ class TestDeliveryTrip(FrappeTestCase):
self.assertEqual(len(route_list[0]), 4)
def test_optimized_route_list_with_locks(self):
- self.delivery_trip.delivery_stops[0].lock = 1
+ self.delivery_trip.delivery_stops[0].locked = 1
self.delivery_trip.save()
route_list = self.delivery_trip.form_route_list(optimize=True)
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
index 0310682a2c..35d1c02719 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
@@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', {
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
&& frm.doc.__onload.has_stock_ledger.length) {
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
- 'type_of_transaction', 'condition', 'mandatory_depends_on'];
+ 'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock'];
frm.fields.forEach((field) => {
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
index eb6102a436..0e4055251f 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
@@ -17,6 +17,8 @@
"target_fieldname",
"applicable_for_documents_tab",
"apply_to_all_doctypes",
+ "column_break_niy2u",
+ "validate_negative_stock",
"column_break_13",
"document_type",
"type_of_transaction",
@@ -173,11 +175,21 @@
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory"
+ },
+ {
+ "fieldname": "column_break_niy2u",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "validate_negative_stock",
+ "fieldtype": "Check",
+ "label": "Validate Negative Stock"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-01-31 13:44:38.507698",
+ "modified": "2023-10-05 12:52:18.705431",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
index 8bff4d5147..257d18fc33 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -60,6 +60,7 @@ class InventoryDimension(Document):
"fetch_from_parent",
"type_of_transaction",
"condition",
+ "validate_negative_stock",
]
for field in frappe.get_meta("Inventory Dimension").fields:
@@ -160,6 +161,7 @@ class InventoryDimension(Document):
insert_after="inventory_dimension",
options=self.reference_document,
label=label,
+ search_index=1,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
),
@@ -255,7 +257,7 @@ def field_exists(doctype, fieldname) -> str or None:
def get_inventory_documents(
doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
):
- and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]]
+ and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No", "Item Price"]]]
or_filters = [
["DocField", "options", "in", ["Batch", "Serial No"]],
["DocField", "parent", "in", ["Putaway Rule"]],
@@ -340,6 +342,7 @@ def get_inventory_dimensions():
fields=[
"distinct target_fieldname as fieldname",
"reference_document as doctype",
+ "validate_negative_stock",
],
filters={"disabled": 0},
)
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
index 2d273c66fa..33394e5a11 100644
--- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -414,6 +414,53 @@ class TestInventoryDimension(FrappeTestCase):
else:
self.assertEqual(d.store, "Inter Transfer Store 2")
+ def test_validate_negative_stock_for_inventory_dimension(self):
+ frappe.local.inventory_dimensions = {}
+ item_code = "Test Negative Inventory Dimension Item"
+ frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
+ create_item(item_code)
+
+ inv_dimension = create_inventory_dimension(
+ apply_to_all_doctypes=1,
+ dimension_name="Inv Site",
+ reference_document="Inv Site",
+ document_type="Inv Site",
+ validate_negative_stock=1,
+ )
+
+ warehouse = create_warehouse("Negative Stock Warehouse")
+ doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
+
+ doc.items[0].to_inv_site = "Site 1"
+ doc.submit()
+
+ site_name = frappe.get_all(
+ "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
+ )[0].inv_site
+
+ self.assertEqual(site_name, "Site 1")
+
+ doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
+
+ doc.items[0].inv_site = "Site 1"
+ self.assertRaises(frappe.ValidationError, doc.submit)
+
+ inv_dimension.reload()
+ inv_dimension.db_set("validate_negative_stock", 0)
+ frappe.local.inventory_dimensions = {}
+
+ doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
+
+ doc.items[0].inv_site = "Site 1"
+ doc.submit()
+ self.assertEqual(doc.docstatus, 1)
+
+ site_name = frappe.get_all(
+ "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
+ )[0].inv_site
+
+ self.assertEqual(site_name, "Site 1")
+
def get_voucher_sl_entries(voucher_no, fields):
return frappe.get_all(
@@ -504,6 +551,26 @@ def prepare_test_data():
}
).insert(ignore_permissions=True)
+ if not frappe.db.exists("DocType", "Inv Site"):
+ frappe.get_doc(
+ {
+ "doctype": "DocType",
+ "name": "Inv Site",
+ "module": "Stock",
+ "custom": 1,
+ "naming_rule": "By fieldname",
+ "autoname": "field:site_name",
+ "fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}],
+ "permissions": [
+ {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
+ ],
+ }
+ ).insert(ignore_permissions=True)
+
+ for site in ["Site 1", "Site 2"]:
+ if not frappe.db.exists("Inv Site", site):
+ frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True)
+
def create_inventory_dimension(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 4ae9bf5b2a..6e810e5987 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -125,36 +125,6 @@ frappe.ui.form.on("Item", {
erpnext.toggle_naming_series();
}
- if (!frm.doc.published_in_website) {
- frm.add_custom_button(__("Publish in Website"), function() {
- frappe.call({
- method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item",
- args: {doc: frm.doc},
- freeze: true,
- freeze_message: __("Publishing Item ..."),
- callback: function(result) {
- frappe.msgprint({
- message: __("Website Item {0} has been created.",
- [repl('
%(item)s ', {
- item_encoded: encodeURIComponent(result.message[0]),
- item: result.message[1]
- })]
- ),
- title: __("Published"),
- indicator: "green"
- });
- }
- });
- }, __('Actions'));
- } else {
- frm.add_custom_button(__("Website Item"), function() {
- frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => {
- if (!d.name) frappe.throw(__("Website Item not found"));
- frappe.set_route("Form", "Website Item", d.name);
- });
- }, __("View"));
- }
-
erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 1bcddfa77e..c13d3ebe0f 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -117,7 +117,6 @@
"customer_code",
"default_item_manufacturer",
"default_manufacturer_part_no",
- "published_in_website",
"total_projected_qty"
],
"fields": [
@@ -380,7 +379,7 @@
"options": "fa fa-rss"
},
{
- "description": "Will also apply for variants unless overrridden",
+ "description": "Will also apply for variants unless overridden",
"fieldname": "reorder_levels",
"fieldtype": "Table",
"label": "Reorder level based on Warehouse",
@@ -815,14 +814,6 @@
"label": "Default Manufacturer Part No",
"read_only": 1
},
- {
- "default": "0",
- "depends_on": "published_in_website",
- "fieldname": "published_in_website",
- "fieldtype": "Check",
- "label": "Published in Website",
- "read_only": 1
- },
{
"default": "1",
"fieldname": "grant_commission",
@@ -970,4 +961,4 @@
"states": [],
"title_field": "item_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index aff958738a..d8935fe203 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -32,7 +32,6 @@ from erpnext.controllers.item_variant import (
make_variant_item_code,
validate_item_variant_attributes,
)
-from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
from erpnext.stock.doctype.item_default.item_default import ItemDefault
@@ -122,10 +121,8 @@ class Item(Document):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
def on_update(self):
- invalidate_cache_for_item(self)
self.update_variants()
self.update_item_price()
- self.update_website_item()
def validate_description(self):
"""Clean HTML description if set"""
@@ -248,29 +245,6 @@ class Item(Document):
if self.stock_uom not in uoms_list:
self.append("uoms", {"uom": self.stock_uom, "conversion_factor": 1})
- def update_website_item(self):
- """Update Website Item if change in Item impacts it."""
- web_item = frappe.db.exists("Website Item", {"item_code": self.item_code})
-
- if web_item:
- changed = {}
- editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", "disabled"]
- doc_before_save = self.get_doc_before_save()
-
- for field in editable_fields:
- if doc_before_save.get(field) != self.get(field):
- if field == "disabled":
- changed["published"] = not self.get(field)
- else:
- changed[field] = self.get(field)
-
- if not changed:
- return
-
- web_item_doc = frappe.get_doc("Website Item", web_item)
- web_item_doc.update(changed)
- web_item_doc.save()
-
def validate_item_tax_net_rate_range(self):
for tax in self.get("taxes"):
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
@@ -281,7 +255,7 @@ class Item(Document):
# add item taxes from template
for d in template.get("taxes"):
- self.append("taxes", {"item_tax_template": d.item_tax_template})
+ self.append("taxes", d)
# copy re-order table if empty
if not self.get("reorder_levels"):
@@ -454,7 +428,6 @@ class Item(Document):
if merge:
self.validate_properties_before_merge(new_name)
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
- self.validate_duplicate_website_item_before_merge(old_name, new_name)
self.delete_old_bins(old_name)
def after_rename(self, old_name, new_name, merge):
@@ -466,9 +439,6 @@ class Item(Document):
title=_("Note"),
)
- if self.published_in_website:
- invalidate_cache_for_item(self)
-
frappe.db.set_value("Item", new_name, "item_code", new_name)
if merge:
@@ -554,27 +524,6 @@ class Item(Document):
)
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
- def validate_duplicate_website_item_before_merge(self, old_name, new_name):
- """
- Block merge if both old and new items have website items against them.
- This is to avoid duplicate website items after merging.
- """
- web_items = frappe.get_all(
- "Website Item",
- filters={"item_code": ["in", [old_name, new_name]]},
- fields=["item_code", "name"],
- )
-
- if len(web_items) <= 1:
- return
-
- old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
- web_item_link = get_link_to_form("Website Item", old_web_item)
- old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
-
- msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
- frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
-
def set_last_purchase_rate(self, new_name):
last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0)
frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate)
@@ -1151,32 +1100,6 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
return out
-def invalidate_cache_for_item(doc):
- """Invalidate Item Group cache and rebuild ItemVariantsCacheManager."""
- invalidate_cache_for(doc, doc.item_group)
-
- if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group:
- invalidate_cache_for(doc, doc.old_item_group)
-
- invalidate_item_variants_cache_for_website(doc)
-
-
-def invalidate_item_variants_cache_for_website(doc):
- """Rebuild ItemVariantsCacheManager via Item or Website Item."""
- from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
-
- item_code = None
- is_web_item = doc.get("published_in_website") or doc.get("published")
- if doc.has_variants and is_web_item:
- item_code = doc.item_code
- elif doc.variant_of and frappe.db.get_value("Item", doc.variant_of, "published_in_website"):
- item_code = doc.variant_of
-
- if item_code:
- item_cache = ItemVariantsCacheManager(item_code)
- item_cache.rebuild_cache()
-
-
def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
return
diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py
index 34bb4d1225..88ae34f228 100644
--- a/erpnext/stock/doctype/item/item_dashboard.py
+++ b/erpnext/stock/doctype/item/item_dashboard.py
@@ -32,6 +32,5 @@ def get_data():
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
{"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
- {"label": _("E-commerce"), "items": ["Website Item"]},
],
}
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 0c6dc77635..09d3dd1dad 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -163,7 +163,7 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item With Item Tax Template",
"tax_category": "_Test Tax Category 2",
- "item_tax_template": None,
+ "item_tax_template": "",
},
{
"item_code": "_Test Item Inherit Group Item Tax Template 1",
@@ -178,7 +178,7 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item Inherit Group Item Tax Template 1",
"tax_category": "_Test Tax Category 2",
- "item_tax_template": None,
+ "item_tax_template": "",
},
{
"item_code": "_Test Item Inherit Group Item Tax Template 2",
@@ -193,7 +193,7 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item Inherit Group Item Tax Template 2",
"tax_category": "_Test Tax Category 2",
- "item_tax_template": None,
+ "item_tax_template": "",
},
{
"item_code": "_Test Item Override Group Item Tax Template",
@@ -208,12 +208,12 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item Override Group Item Tax Template",
"tax_category": "_Test Tax Category 2",
- "item_tax_template": None,
+ "item_tax_template": "",
},
]
expected_item_tax_map = {
- None: {},
+ "": {},
"_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10},
"_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12},
"_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15},
@@ -907,6 +907,8 @@ def create_item(
opening_stock=0,
is_fixed_asset=0,
asset_category=None,
+ buying_cost_center=None,
+ selling_cost_center=None,
company="_Test Company",
):
if not frappe.db.exists("Item", item_code):
@@ -924,7 +926,15 @@ def create_item(
item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item
item.customer = customer or ""
- item.append("item_defaults", {"default_warehouse": warehouse, "company": company})
+ item.append(
+ "item_defaults",
+ {
+ "default_warehouse": warehouse,
+ "company": company,
+ "selling_cost_center": selling_cost_center,
+ "buying_cost_center": buying_cost_center,
+ },
+ )
item.save()
else:
item = frappe.get_doc("Item", item_code)
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index ce489ff52b..8a4b4eef0a 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -6,7 +6,6 @@ frappe.ui.form.on("Item Price", {
frm.set_query("item_code", function() {
return {
filters: {
- "disabled": 0,
"has_variants": 0
}
};
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index bf3301f6d8..9673a70501 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -102,6 +102,12 @@ frappe.ui.form.on('Material Request', {
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
let precision = frappe.defaults.get_default("float_precision");
+
+ if (flt(frm.doc.per_received, precision) < 100) {
+ frm.add_custom_button(__('Stop'),
+ () => frm.events.update_status(frm, 'Stopped'));
+ }
+
if (flt(frm.doc.per_ordered, precision) < 100) {
let add_create_pick_list_button = () => {
frm.add_custom_button(__('Pick List'),
@@ -148,11 +154,6 @@ frappe.ui.form.on('Material Request', {
}
frm.page.set_inner_btn_group_as_primary(__('Create'));
-
- // stop
- frm.add_custom_button(__('Stop'),
- () => frm.events.update_status(frm, 'Stopped'));
-
}
}
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index a51028da19..ecdec800e5 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -401,6 +401,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
["uom", "uom"],
["sales_order", "sales_order"],
["sales_order_item", "sales_order_item"],
+ ["wip_composite_asset", "wip_composite_asset"],
],
"postprocess": update_item,
"condition": select_item,
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index c585d6c490..9912be145f 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -37,6 +37,10 @@
"rate",
"col_break3",
"amount",
+ "accounting_details_section",
+ "expense_account",
+ "column_break_glru",
+ "wip_composite_asset",
"manufacture_details",
"manufacturer",
"manufacturer_part_no",
@@ -50,11 +54,10 @@
"lead_time_date",
"sales_order",
"sales_order_item",
+ "col_break4",
"production_plan",
"material_request_plan_item",
"job_card_item",
- "col_break4",
- "expense_account",
"section_break_46",
"page_break"
],
@@ -454,13 +457,28 @@
"label": "Job Card Item",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "column_break_glru",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "wip_composite_asset",
+ "fieldtype": "Link",
+ "label": "WIP Composite Asset",
+ "options": "Asset"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-05-07 20:23:31.250252",
+ "modified": "2023-10-27 15:53:41.444236",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
@@ -471,4 +489,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index ae05b80727..7cd171ea92 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -265,7 +265,8 @@ frappe.ui.form.on('Pick List', {
from_date: moment(frm.doc.creation).format('YYYY-MM-DD'),
to_date: to_date,
voucher_type: "Sales Order",
- against_pick_list: frm.doc.name,
+ from_voucher_type: "Pick List",
+ from_voucher_no: frm.doc.name,
}
frappe.set_route("query-report", "Reserved Stock");
}
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 2fcd1025a0..ed20209577 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -229,20 +229,27 @@ class PickList(Document):
def create_stock_reservation_entries(self, notify=True) -> None:
"""Creates Stock Reservation Entries for Sales Order Items against Pick List."""
- from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
- create_stock_reservation_entries_for_so_items,
- )
-
- so_details = {}
+ so_items_details_map = {}
for location in self.locations:
if location.warehouse and location.sales_order and location.sales_order_item:
- so_details.setdefault(location.sales_order, []).append(location)
+ item_details = {
+ "name": location.sales_order_item,
+ "item_code": location.item_code,
+ "warehouse": location.warehouse,
+ "qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
+ "from_voucher_no": location.parent,
+ "from_voucher_detail_no": location.name,
+ "serial_and_batch_bundle": location.serial_and_batch_bundle,
+ }
+ so_items_details_map.setdefault(location.sales_order, []).append(item_details)
- if so_details:
- for so, locations in so_details.items():
+ if so_items_details_map:
+ for so, items_details in so_items_details_map.items():
so_doc = frappe.get_doc("Sales Order", so)
- create_stock_reservation_entries_for_so_items(
- so=so_doc, items_details=locations, against_pick_list=True, notify=notify
+ so_doc.create_stock_reservation_entries(
+ items_details=items_details,
+ from_voucher_type="Pick List",
+ notify=notify,
)
@frappe.whitelist()
@@ -253,7 +260,9 @@ class PickList(Document):
cancel_stock_reservation_entries,
)
- cancel_stock_reservation_entries(against_pick_list=self.name, notify=notify)
+ cancel_stock_reservation_entries(
+ from_voucher_type="Pick List", from_voucher_no=self.name, notify=notify
+ )
def validate_picked_qty(self, data):
over_delivery_receipt_allowance = 100 + flt(
diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
index 0830fa2143..29571a5400 100644
--- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
+++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
@@ -2,7 +2,7 @@ def get_data():
return {
"fieldname": "pick_list",
"non_standard_fieldnames": {
- "Stock Reservation Entry": "against_pick_list",
+ "Stock Reservation Entry": "from_voucher_no",
},
"internal_links": {
"Sales Order": ["locations", "sales_order"],
diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py
index e77d53a367..21c0f18cc3 100644
--- a/erpnext/stock/doctype/price_list/price_list.py
+++ b/erpnext/stock/doctype/price_list/price_list.py
@@ -13,9 +13,6 @@ class PriceList(Document):
if not cint(self.buying) and not cint(self.selling):
throw(_("Price List must be applicable for Buying or Selling"))
- if not self.is_new():
- self.check_impact_on_shopping_cart()
-
def on_update(self):
self.set_default_if_missing()
self.update_item_price()
@@ -37,19 +34,6 @@ class PriceList(Document):
(self.currency, cint(self.buying), cint(self.selling), self.name),
)
- def check_impact_on_shopping_cart(self):
- "Check if Price List currency change impacts E Commerce Cart."
- from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
- validate_cart_settings,
- )
-
- doc_before_save = self.get_doc_before_save()
- currency_changed = self.currency != doc_before_save.currency
- affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list")
-
- if currency_changed and affects_cart:
- validate_cart_settings()
-
def on_trash(self):
self.delete_price_list_details_key()
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 6afa86e34e..2a4b6f34b5 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -13,7 +13,6 @@ from pypika import functions as fn
import erpnext
from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
-from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
@@ -144,8 +143,8 @@ class PurchaseReceipt(BuyingController):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
# check cwip accounts before making auto assets
# Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account
- arbnb_account = self.get_company_default("asset_received_but_not_billed")
- cwip_account = get_asset_account(
+ self.get_company_default("asset_received_but_not_billed")
+ get_asset_account(
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
)
break
@@ -264,6 +263,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.set_consumed_qty_in_subcontract_order()
+ self.reserve_stock_for_sales_order()
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
@@ -313,7 +313,7 @@ class PurchaseReceipt(BuyingController):
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
self.make_tax_gl_entries(gl_entries)
- self.get_asset_gl_entry(gl_entries)
+ update_regional_gl_entries(gl_entries, self)
return process_gl_map(gl_entries)
@@ -322,14 +322,6 @@ class PurchaseReceipt(BuyingController):
get_purchase_document_details,
)
- stock_rbnb = None
- if erpnext.is_perpetual_inventory_enabled(self.company):
- stock_rbnb = self.get_company_default("stock_received_but_not_billed")
- landed_cost_entries = get_item_account_wise_additional_cost(self.name)
- expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
-
- warehouse_with_no_account = []
- stock_items = self.get_stock_items()
provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
@@ -338,28 +330,258 @@ class PurchaseReceipt(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
+ def validate_account(account_type):
+ frappe.throw(_("{0} account not found while submitting purchase receipt").format(account_type))
+
+ def make_item_asset_inward_gl_entry(item, stock_value_diff, stock_asset_account_name):
+ account_currency = get_account_currency(stock_asset_account_name)
+
+ if not stock_asset_account_name:
+ validate_account("Asset or warehouse account")
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=stock_asset_account_name,
+ cost_center=d.cost_center,
+ debit=stock_value_diff,
+ credit=0.0,
+ remarks=remarks,
+ against_account=stock_asset_rbnb,
+ account_currency=account_currency,
+ item=item,
+ )
+
+ def make_stock_received_but_not_billed_entry(item):
+ account = (
+ warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb
+ )
+ account_currency = get_account_currency(account)
+
+ # GL Entry for from warehouse or Stock Received but not billed
+ # Intentionally passed negative debit amount to avoid incorrect GL Entry validation
+ credit_amount = (
+ flt(item.base_net_amount, item.precision("base_net_amount"))
+ if account_currency == self.company_currency
+ else flt(item.net_amount, item.precision("net_amount"))
+ )
+
+ outgoing_amount = item.base_net_amount
+ if self.is_internal_transfer() and item.valuation_rate:
+ outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
+ credit_amount = outgoing_amount
+
+ if credit_amount:
+ if not account:
+ validate_account("Stock or Asset Received But Not Billed")
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
+ credit=0.0,
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
+ account_currency=account_currency,
+ item=item,
+ )
+
+ # check if the exchange rate has changed
+ if d.get("purchase_invoice"):
+ if (
+ exchange_rate_map[item.purchase_invoice]
+ and self.conversion_rate != exchange_rate_map[item.purchase_invoice]
+ and item.net_rate == net_rate_map[item.purchase_invoice_item]
+ ):
+
+ discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
+ exchange_rate_map[item.purchase_invoice] - self.conversion_rate
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=discrepancy_caused_by_exchange_rate_difference,
+ remarks=remarks,
+ against_account=self.supplier,
+ debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ account_currency=account_currency,
+ item=item,
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=self.get_company_default("exchange_gain_loss_account"),
+ cost_center=d.cost_center,
+ debit=discrepancy_caused_by_exchange_rate_difference,
+ credit=0.0,
+ remarks=remarks,
+ against_account=self.supplier,
+ debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ account_currency=account_currency,
+ item=item,
+ )
+
+ return outgoing_amount
+
+ def make_landed_cost_gl_entries(item):
+ # Amount added through landed-cost-voucher
+ if item.landed_cost_voucher_amount and landed_cost_entries:
+ if (item.item_code, item.name) in landed_cost_entries:
+ for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
+ account_currency = get_account_currency(account)
+ credit_amount = (
+ flt(amount["base_amount"])
+ if (amount["base_amount"] or account_currency != self.company_currency)
+ else flt(amount["amount"])
+ )
+
+ if not account:
+ validate_account("Landed Cost Account")
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=credit_amount,
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ credit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ def make_rate_difference_entry(item):
+ if item.rate_difference_with_purchase_invoice and stock_asset_rbnb:
+ account_currency = get_account_currency(stock_asset_rbnb)
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=stock_asset_rbnb,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=flt(item.rate_difference_with_purchase_invoice),
+ remarks=_("Adjustment based on Purchase Invoice rate"),
+ against_account=stock_asset_account_name,
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ def make_sub_contracting_gl_entries(item):
+ # sub-contracting warehouse
+ if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=supplier_warehouse_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=flt(item.rm_supp_cost),
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ account_currency=supplier_warehouse_account_currency,
+ item=item,
+ )
+
+ def make_divisional_loss_gl_entry(item, outgoing_amount):
+ if item.is_fixed_asset:
+ return
+
+ # divisional loss adjustment
+ valuation_amount_as_per_doc = (
+ flt(outgoing_amount, d.precision("base_net_amount"))
+ + flt(item.landed_cost_voucher_amount)
+ + flt(item.rm_supp_cost)
+ + flt(item.item_tax_amount)
+ + flt(item.rate_difference_with_purchase_invoice)
+ )
+
+ divisional_loss = flt(
+ valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount")
+ )
+
+ if divisional_loss:
+ loss_account = (
+ self.get_company_default("default_expense_account", ignore_validation=True)
+ or stock_asset_rbnb
+ )
+
+ cost_center = item.cost_center or frappe.get_cached_value(
+ "Company", self.company, "cost_center"
+ )
+ account_currency = get_account_currency(loss_account)
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=loss_account,
+ cost_center=cost_center,
+ debit=divisional_loss,
+ credit=0.0,
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ stock_items = self.get_stock_items()
+ warehouse_with_no_account = []
+
for d in self.get("items"):
- if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
- if warehouse_account.get(d.warehouse):
- stock_value_diff = frappe.db.get_value(
- "Stock Ledger Entry",
- {
- "voucher_type": "Purchase Receipt",
- "voucher_no": self.name,
- "voucher_detail_no": d.name,
- "warehouse": d.warehouse,
- "is_cancelled": 0,
- },
- "stock_value_difference",
+ if (
+ provisional_accounting_for_non_stock_items
+ and d.item_code not in stock_items
+ and flt(d.qty)
+ and d.get("provisional_expense_account")
+ and not d.is_fixed_asset
+ ):
+ self.add_provisional_gl_entry(
+ d, gl_entries, self.posting_date, d.get("provisional_expense_account")
+ )
+ elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return):
+ remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
+ "Asset" if d.is_fixed_asset else "Stock"
+ )
+
+ if not (
+ (erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
+ or d.is_fixed_asset
+ ):
+ continue
+
+ stock_asset_rbnb = (
+ self.get_company_default("asset_received_but_not_billed")
+ if d.is_fixed_asset
+ else self.get_company_default("stock_received_but_not_billed")
+ )
+ landed_cost_entries = get_item_account_wise_additional_cost(self.name)
+
+ if d.is_fixed_asset:
+ account_type = (
+ "capital_work_in_progress_account"
+ if is_cwip_accounting_enabled(d.asset_category)
+ else "fixed_asset_account"
)
- warehouse_account_name = warehouse_account[d.warehouse]["account"]
- warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
+ stock_asset_account_name = get_asset_account(
+ account_type, asset_category=d.asset_category, company=self.company
+ )
+
+ stock_value_diff = (
+ flt(d.net_amount)
+ + flt(d.item_tax_amount / self.conversion_rate)
+ + flt(d.landed_cost_voucher_amount)
+ )
+ elif warehouse_account.get(d.warehouse):
+ stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
+ stock_asset_account_name = warehouse_account[d.warehouse]["account"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get(
"account_currency"
)
- remarks = self.get("remarks") or _("Accounting Entry for Stock")
# If PR is sub-contracted and fg item rate is zero
# in that case if account for source and target warehouse are same,
@@ -367,214 +589,25 @@ class PurchaseReceipt(BuyingController):
if (
flt(stock_value_diff) == flt(d.rm_supp_cost)
and warehouse_account.get(self.supplier_warehouse)
- and warehouse_account_name == supplier_warehouse_account
+ and stock_asset_account_name == supplier_warehouse_account
):
continue
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=warehouse_account_name,
- cost_center=d.cost_center,
- debit=stock_value_diff,
- credit=0.0,
- remarks=remarks,
- against_account=stock_rbnb,
- account_currency=warehouse_account_currency,
- item=d,
- )
-
- # GL Entry for from warehouse or Stock Received but not billed
- # Intentionally passed negative debit amount to avoid incorrect GL Entry validation
- credit_currency = (
- get_account_currency(warehouse_account[d.from_warehouse]["account"])
- if d.from_warehouse
- else get_account_currency(stock_rbnb)
- )
-
- credit_amount = (
- flt(d.base_net_amount, d.precision("base_net_amount"))
- if credit_currency == self.company_currency
- else flt(d.net_amount, d.precision("net_amount"))
- )
-
- outgoing_amount = d.base_net_amount
- if self.is_internal_transfer() and d.valuation_rate:
- outgoing_amount = abs(
- frappe.db.get_value(
- "Stock Ledger Entry",
- {
- "voucher_type": "Purchase Receipt",
- "voucher_no": self.name,
- "voucher_detail_no": d.name,
- "warehouse": d.from_warehouse,
- "is_cancelled": 0,
- },
- "stock_value_difference",
- )
- )
- credit_amount = outgoing_amount
-
- if credit_amount:
- account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=d.cost_center,
- debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")),
- credit=0.0,
- remarks=remarks,
- against_account=warehouse_account_name,
- debit_in_account_currency=-1 * credit_amount,
- account_currency=credit_currency,
- item=d,
- )
-
- # check if the exchange rate has changed
- if d.get("purchase_invoice"):
- if (
- exchange_rate_map[d.purchase_invoice]
- and self.conversion_rate != exchange_rate_map[d.purchase_invoice]
- and d.net_rate == net_rate_map[d.purchase_invoice_item]
- ):
-
- discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * (
- exchange_rate_map[d.purchase_invoice] - self.conversion_rate
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=d.cost_center,
- debit=0.0,
- credit=discrepancy_caused_by_exchange_rate_difference,
- remarks=remarks,
- against_account=self.supplier,
- debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
- account_currency=credit_currency,
- item=d,
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=self.get_company_default("exchange_gain_loss_account"),
- cost_center=d.cost_center,
- debit=discrepancy_caused_by_exchange_rate_difference,
- credit=0.0,
- remarks=remarks,
- against_account=self.supplier,
- debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
- account_currency=credit_currency,
- item=d,
- )
-
- # Amount added through landed-cos-voucher
- if d.landed_cost_voucher_amount and landed_cost_entries:
- if (d.item_code, d.name) in landed_cost_entries:
- for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
- account_currency = get_account_currency(account)
- credit_amount = (
- flt(amount["base_amount"])
- if (amount["base_amount"] or account_currency != self.company_currency)
- else flt(amount["amount"])
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=d.cost_center,
- debit=0.0,
- credit=credit_amount,
- remarks=remarks,
- against_account=warehouse_account_name,
- credit_in_account_currency=flt(amount["amount"]),
- account_currency=account_currency,
- project=d.project,
- item=d,
- )
-
- if d.rate_difference_with_purchase_invoice and stock_rbnb:
- account_currency = get_account_currency(stock_rbnb)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=stock_rbnb,
- cost_center=d.cost_center,
- debit=0.0,
- credit=flt(d.rate_difference_with_purchase_invoice),
- remarks=_("Adjustment based on Purchase Invoice rate"),
- against_account=warehouse_account_name,
- account_currency=account_currency,
- project=d.project,
- item=d,
- )
-
- # sub-contracting warehouse
- if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=supplier_warehouse_account,
- cost_center=d.cost_center,
- debit=0.0,
- credit=flt(d.rm_supp_cost),
- remarks=remarks,
- against_account=warehouse_account_name,
- account_currency=supplier_warehouse_account_currency,
- item=d,
- )
-
- # divisional loss adjustment
- valuation_amount_as_per_doc = (
- flt(outgoing_amount, d.precision("base_net_amount"))
- + flt(d.landed_cost_voucher_amount)
- + flt(d.rm_supp_cost)
- + flt(d.item_tax_amount)
- + flt(d.rate_difference_with_purchase_invoice)
- )
-
- divisional_loss = flt(
- valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
- )
-
- if divisional_loss:
- if self.is_return or flt(d.item_tax_amount):
- loss_account = expenses_included_in_valuation
- else:
- loss_account = (
- self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb
- )
-
- cost_center = d.cost_center or frappe.get_cached_value(
- "Company", self.company, "cost_center"
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=loss_account,
- cost_center=cost_center,
- debit=divisional_loss,
- credit=0.0,
- remarks=remarks,
- against_account=warehouse_account_name,
- account_currency=credit_currency,
- project=d.project,
- item=d,
- )
-
- elif (
- d.warehouse not in warehouse_with_no_account
- or d.rejected_warehouse not in warehouse_with_no_account
- ):
- warehouse_with_no_account.append(d.warehouse)
+ if (flt(d.valuation_rate) or self.is_return or d.is_fixed_asset) and flt(d.qty):
+ make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
+ outgoing_amount = make_stock_received_but_not_billed_entry(d)
+ make_landed_cost_gl_entries(d)
+ make_rate_difference_entry(d)
+ make_sub_contracting_gl_entries(d)
+ make_divisional_loss_gl_entry(d, outgoing_amount)
elif (
- d.item_code not in stock_items
- and not d.is_fixed_asset
- and flt(d.qty)
- and provisional_accounting_for_non_stock_items
- and d.get("provisional_expense_account")
+ d.warehouse not in warehouse_with_no_account
+ or d.rejected_warehouse not in warehouse_with_no_account
):
- self.add_provisional_gl_entry(
- d, gl_entries, self.posting_date, d.get("provisional_expense_account")
- )
+ warehouse_with_no_account.append(d.warehouse)
+
+ if d.is_fixed_asset:
+ self.update_assets(d, d.valuation_rate)
if warehouse_with_no_account:
frappe.msgprint(
@@ -587,8 +620,8 @@ class PurchaseReceipt(BuyingController):
self, item, gl_entries, posting_date, provisional_account, reverse=0
):
credit_currency = get_account_currency(provisional_account)
- debit_currency = get_account_currency(item.expense_account)
expense_account = item.expense_account
+ debit_currency = get_account_currency(item.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1
@@ -629,11 +662,8 @@ class PurchaseReceipt(BuyingController):
)
def make_tax_gl_entries(self, gl_entries):
-
- if erpnext.is_perpetual_inventory_enabled(self.company):
- expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
-
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
+ is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
# Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {}
for tax in self.get("taxes"):
@@ -653,26 +683,26 @@ class PurchaseReceipt(BuyingController):
if negative_expense_to_be_booked and valuation_tax:
# Backward compatibility:
- # If expenses_included_in_valuation account has been credited in against PI
# and charges added via Landed Cost Voucher,
# post valuation related charges on "Stock Received But Not Billed"
- # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account
-
- negative_expense_booked_in_pi = frappe.db.sql(
- """select name from `tabPurchase Invoice Item` pi
- where docstatus = 1 and purchase_receipt=%s
- and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
- and voucher_no=pi.parent and account=%s)""",
- (self.name, expenses_included_in_valuation),
- )
-
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
- stock_rbnb = self.get_company_default("stock_received_but_not_billed")
+ stock_rbnb = (
+ self.get("asset_received_but_not_billed")
+ if is_asset_pr
+ else self.get_company_default("stock_received_but_not_billed")
+ )
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
+ negative_expense_booked_in_pi = frappe.db.sql(
+ """select name from `tabPurchase Invoice Item` pi
+ where docstatus = 1 and purchase_receipt=%s
+ and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
+ and voucher_no=pi.parent and account=%s)""",
+ (self.name, tax.account_head),
+ )
if negative_expense_booked_in_pi:
account = stock_rbnb
@@ -700,103 +730,6 @@ class PurchaseReceipt(BuyingController):
i += 1
- def get_asset_gl_entry(self, gl_entries):
- for item in self.get("items"):
- if item.is_fixed_asset:
- if is_cwip_accounting_enabled(item.asset_category):
- self.add_asset_gl_entries(item, gl_entries)
- if flt(item.landed_cost_voucher_amount):
- self.add_lcv_gl_entries(item, gl_entries)
- # update assets gross amount by its valuation rate
- # valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item
- self.update_assets(item, item.valuation_rate)
- return gl_entries
-
- def add_asset_gl_entries(self, item, gl_entries):
- arbnb_account = self.get_company_default("asset_received_but_not_billed")
- # This returns category's cwip account if not then fallback to company's default cwip account
- cwip_account = get_asset_account(
- "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
- )
-
- asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
- base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
- remarks = self.get("remarks") or _("Accounting Entry for Asset")
-
- cwip_account_currency = get_account_currency(cwip_account)
- # debit cwip account
- debit_in_account_currency = (
- base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=cwip_account,
- cost_center=item.cost_center,
- debit=base_asset_amount,
- credit=0.0,
- remarks=remarks,
- against_account=arbnb_account,
- debit_in_account_currency=debit_in_account_currency,
- item=item,
- )
-
- asset_rbnb_currency = get_account_currency(arbnb_account)
- # credit arbnb account
- credit_in_account_currency = (
- base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=arbnb_account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=base_asset_amount,
- remarks=remarks,
- against_account=cwip_account,
- credit_in_account_currency=credit_in_account_currency,
- item=item,
- )
-
- def add_lcv_gl_entries(self, item, gl_entries):
- expenses_included_in_asset_valuation = self.get_company_default(
- "expenses_included_in_asset_valuation"
- )
- if not is_cwip_accounting_enabled(item.asset_category):
- asset_account = get_asset_category_account(
- asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company
- )
- else:
- # This returns company's default cwip account
- asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
-
- remarks = self.get("remarks") or _("Accounting Entry for Stock")
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=expenses_included_in_asset_valuation,
- cost_center=item.cost_center,
- debit=0.0,
- credit=flt(item.landed_cost_voucher_amount),
- remarks=remarks,
- against_account=asset_account,
- project=item.project,
- item=item,
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=asset_account,
- cost_center=item.cost_center,
- debit=flt(item.landed_cost_voucher_amount),
- credit=0.0,
- remarks=remarks,
- against_account=expenses_included_in_asset_valuation,
- project=item.project,
- item=item,
- )
-
def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all(
"Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code}
@@ -821,16 +754,59 @@ class PurchaseReceipt(BuyingController):
po_details.append(d.purchase_order_item)
if po_details:
- updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
+ updated_pr += update_billed_amount_based_on_po(po_details, update_modified, self)
for pr in set(updated_pr):
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
- self.load_from_db()
+ def reserve_stock_for_sales_order(self):
+ if self.is_return or not cint(
+ frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase")
+ ):
+ return
+
+ self.reload() # reload to get the Serial and Batch Bundle Details
+
+ so_items_details_map = {}
+ for item in self.items:
+ if item.sales_order and item.sales_order_item:
+ item_details = {
+ "name": item.sales_order_item,
+ "item_code": item.item_code,
+ "warehouse": item.warehouse,
+ "qty_to_reserve": item.stock_qty,
+ "from_voucher_no": item.parent,
+ "from_voucher_detail_no": item.name,
+ "serial_and_batch_bundle": item.serial_and_batch_bundle,
+ }
+ so_items_details_map.setdefault(item.sales_order, []).append(item_details)
+
+ if so_items_details_map:
+ for so, items_details in so_items_details_map.items():
+ so_doc = frappe.get_doc("Sales Order", so)
+ so_doc.create_stock_reservation_entries(
+ items_details=items_details,
+ from_voucher_type="Purchase Receipt",
+ notify=True,
+ )
-def update_billed_amount_based_on_po(po_details, update_modified=True):
+def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
+ return frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": voucher_no,
+ "voucher_detail_no": voucher_detail_no,
+ "warehouse": warehouse,
+ "is_cancelled": 0,
+ },
+ "stock_value_difference",
+ )
+
+
+def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None):
po_billed_amt_details = get_billed_amount_against_po(po_details)
# Get all Purchase Receipt Item rows against the Purchase Order Items
@@ -859,13 +835,19 @@ def update_billed_amount_based_on_po(po_details, update_modified=True):
po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po
if pr_item.billed_amt != billed_amt_agianst_pr:
- frappe.db.set_value(
- "Purchase Receipt Item",
- pr_item.name,
- "billed_amt",
- billed_amt_agianst_pr,
- update_modified=update_modified,
- )
+ # update existing doc if possible
+ if pr_doc and pr_item.parent == pr_doc.name:
+ pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None)
+ pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified)
+
+ else:
+ frappe.db.set_value(
+ "Purchase Receipt Item",
+ pr_item.name,
+ "billed_amt",
+ billed_amt_agianst_pr,
+ update_modified=update_modified,
+ )
updated_pr.append(pr_item.parent)
@@ -941,9 +923,6 @@ def get_billed_amount_against_po(po_items):
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
- # Reload as billed amount was set in db directly
- pr_doc.load_from_db()
-
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
@@ -969,7 +948,6 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
- pr_doc.load_from_db()
if update_modified:
pr_doc.set_status(update=True)
@@ -1091,6 +1069,7 @@ def make_purchase_invoice(source_name, target_doc=None):
"is_fixed_asset": "is_fixed_asset",
"asset_location": "asset_location",
"asset_category": "asset_category",
+ "wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"filter": lambda d: get_pending_qty(d)[0] <= 0
@@ -1255,3 +1234,8 @@ def get_item_account_wise_additional_cost(purchase_document):
def on_doctype_update():
frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
+
+
+@erpnext.allow_regional
+def update_regional_gl_entries(gl_list, doc):
+ return
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
index b3ae7b58b4..71489fbb49 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
@@ -10,6 +10,7 @@ def get_data():
"Landed Cost Voucher": "receipt_document",
"Auto Repeat": "reference_document",
"Purchase Receipt": "return_against",
+ "Stock Reservation Entry": "from_voucher_no",
},
"internal_links": {
"Material Request": ["items", "material_request"],
@@ -18,7 +19,10 @@ def get_data():
"Quality Inspection": ["items", "quality_inspection"],
},
"transactions": [
- {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]},
+ {
+ "label": _("Related"),
+ "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset", "Stock Reservation Entry"],
+ },
{
"label": _("Reference"),
"items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index a8ef5e8e48..146cbff1aa 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -958,17 +958,33 @@ class TestPurchaseReceipt(FrappeTestCase):
pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
pr1 = make_purchase_receipt(
- warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory"
+ warehouse="Stores - TCP1", company="_Test Company with perpetual inventory"
)
- pr = make_purchase_receipt(
- company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
+ dn1 = create_delivery_note(
+ item_code=pr1.items[0].item_code,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=5,
+ rate=500,
+ warehouse="Stores - TCP1",
+ target_warehouse="Work In Progress - TCP1",
)
- pr.supplier_warehouse = ""
+ pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "Work In Progress - TCP1"
-
+ pr.items[0].warehouse = "Stores - TCP1"
pr.submit()
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -982,9 +998,13 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
pr.cancel()
- pr1.cancel()
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+
create_warehouse(
"_Test Warehouse for Valuation",
company="_Test Company with perpetual inventory",
@@ -992,16 +1012,28 @@ class TestPurchaseReceipt(FrappeTestCase):
)
pr1 = make_purchase_receipt(
- warehouse="_Test Warehouse for Valuation - TCP1",
+ warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory",
)
- pr = make_purchase_receipt(
- company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ dn1 = create_delivery_note(
+ item_code=pr1.items[0].item_code,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=5,
+ rate=50,
+ warehouse="Stores - TCP1",
+ target_warehouse="_Test Warehouse for Valuation - TCP1",
)
+ pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1"
- pr.supplier_warehouse = ""
+ pr.items[0].warehouse = "Stores - TCP1"
pr.append(
"taxes",
@@ -1037,7 +1069,6 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(gle.credit, expected_gle[i][2])
pr.cancel()
- pr1.cancel()
def test_po_to_pi_and_po_to_pr_worflow_full(self):
"""Test following behaviour:
@@ -2086,6 +2117,77 @@ class TestPurchaseReceipt(FrappeTestCase):
return_pr.reload()
self.assertEqual(return_pr.status, "Completed")
+ def test_purchase_return_with_zero_rate(self):
+ company = "_Test Company with perpetual inventory"
+
+ # Step - 1: Create Item
+ item, warehouse = (
+ make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name,
+ "Stores - TCP1",
+ )
+
+ # Step - 2: Create Stock Entry (Material Receipt)
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ se = make_stock_entry(
+ purpose="Material Receipt",
+ item_code=item,
+ qty=100,
+ basic_rate=100,
+ to_warehouse=warehouse,
+ company=company,
+ )
+
+ # Step - 3: Create Purchase Receipt
+ pr = make_purchase_receipt(
+ item_code=item,
+ qty=5,
+ rate=0,
+ warehouse=warehouse,
+ company=company,
+ )
+
+ # Step - 4: Create Purchase Return
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ pr_return = make_return_doc("Purchase Receipt", pr.name)
+ pr_return.save()
+ pr_return.submit()
+
+ sl_entries = get_sl_entries(pr_return.doctype, pr_return.name)
+ gl_entries = get_gl_entries(pr_return.doctype, pr_return.name)
+
+ # Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate
+ average_rate = (
+ (se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate)
+ ) / (se.items[0].qty + pr.items[0].qty)
+ expected_stock_value_difference = pr_return.items[0].qty * average_rate
+ self.assertEqual(
+ flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2)
+ )
+
+ # Test - 2: GL Entries should be created for Stock Value Difference
+ self.assertEqual(len(gl_entries), 2)
+
+ # Test - 3: SLE Stock Value Difference should be equal to Debit or Credit of GL Entries.
+ for entry in gl_entries:
+ self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference))
+
+ def non_internal_transfer_purchase_receipt(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ pr_doc = make_purchase_receipt(do_not_submit=True)
+ warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company)
+ pr_doc.items[0].db_set("target_warehouse", "warehouse")
+
+ pr_doc.reload()
+
+ self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name)
+
+ pr_doc.save()
+ pr_doc.reload()
+ self.assertFalse(pr_doc.items[0].from_warehouse)
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index d93d21c1f2..f5240a6094 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -125,7 +125,9 @@
"dimension_col_break",
"cost_center",
"section_break_80",
- "page_break"
+ "page_break",
+ "sales_order",
+ "sales_order_item"
],
"fields": [
{
@@ -1062,12 +1064,32 @@
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
+ },
+ {
+ "fieldname": "sales_order",
+ "fieldtype": "Link",
+ "label": "Sales Order",
+ "no_copy": 1,
+ "options": "Sales Order",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "sales_order_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Sales Order Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-10-03 21:11:50.547261",
+ "modified": "2023-10-19 10:50:58.071735",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
@@ -1078,4 +1100,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 96e4a55630..f96c184c88 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -658,7 +658,7 @@ class SerialandBatchBundle(Document):
if not available_batches:
return
- available_batches = get_availabel_batches_qty(available_batches)
+ available_batches = get_available_batches_qty(available_batches)
for batch_no in batches:
if batch_no not in available_batches or available_batches[batch_no] < 0:
self.throw_error_message(
@@ -1074,7 +1074,7 @@ def get_auto_data(**kwargs):
return get_auto_batch_nos(kwargs)
-def get_availabel_batches_qty(available_batches):
+def get_available_batches_qty(available_batches):
available_batches_qty = defaultdict(float)
for batch in available_batches:
available_batches_qty[batch.batch_no] += batch.qty
@@ -1301,6 +1301,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
"POS Invoice",
fields=[
"`tabPOS Invoice Item`.batch_no",
+ "`tabPOS Invoice Item`.qty",
"`tabPOS Invoice`.is_return",
"`tabPOS Invoice Item`.warehouse",
"`tabPOS Invoice Item`.name as child_docname",
@@ -1321,9 +1322,6 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
if pos_invoice.serial_and_batch_bundle
]
- if not ids:
- return {}
-
if ids:
for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids):
key = (d.batch_no, d.warehouse)
@@ -1337,6 +1335,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
else:
pos_batches[key].qty += d.qty
+ # POS invoices having batch without bundle (to handle old POS invoices)
for row in pos_invoices:
if not row.batch_no:
continue
@@ -1346,11 +1345,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
key = (row.batch_no, row.warehouse)
if key in pos_batches:
- pos_batches[key] -= row.qty * -1 if row.is_return else row.qty
+ pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty
else:
pos_batches[key] = frappe._dict(
{
- "qty": (row.qty * -1 if row.is_return else row.qty),
+ "qty": (row.qty * -1 if not row.is_return else row.qty),
"warehouse": row.warehouse,
}
)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a2cae7ff8d..c41349fcfb 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -438,31 +438,37 @@ class StockEntry(StockController):
item_code.append(item.item_code)
def validate_fg_completed_qty(self):
- item_wise_qty = {}
- if self.purpose == "Manufacture" and self.work_order:
- for d in self.items:
- if d.is_finished_item:
- if self.process_loss_qty:
- d.qty = self.fg_completed_qty - self.process_loss_qty
+ if self.purpose != "Manufacture":
+ return
- item_wise_qty.setdefault(d.item_code, []).append(d.qty)
+ fg_qty = defaultdict(float)
+ for d in self.items:
+ if d.is_finished_item:
+ fg_qty[d.item_code] += flt(d.qty)
+
+ if not fg_qty:
+ return
precision = frappe.get_precision("Stock Entry Detail", "qty")
- for item_code, qty_list in item_wise_qty.items():
- total = flt(sum(qty_list), precision)
+ fg_item = list(fg_qty.keys())[0]
+ fg_item_qty = flt(fg_qty[fg_item], precision)
+ fg_completed_qty = flt(self.fg_completed_qty, precision)
- if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty:
- self.process_loss_qty = flt(self.fg_completed_qty - total, precision)
- self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
+ for d in self.items:
+ if not fg_qty.get(d.item_code):
+ continue
- if self.process_loss_qty:
- total += flt(self.process_loss_qty, precision)
+ if (fg_completed_qty - fg_item_qty) > 0:
+ self.process_loss_qty = fg_completed_qty - fg_item_qty
- if self.fg_completed_qty != total:
+ if not self.process_loss_qty:
+ continue
+
+ if fg_completed_qty != (flt(fg_item_qty) + flt(self.process_loss_qty, precision)):
frappe.throw(
- _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format(
- frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty)
- )
+ _(
+ "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table."
+ ).format(frappe.bold(self.process_loss_qty), frappe.bold(d.item_code))
)
def validate_difference_account(self):
@@ -1014,14 +1020,34 @@ class StockEntry(StockController):
& (se.docstatus == 1)
& (se_detail.item_code == se_item.item_code)
& (
- (se.purchase_order == self.purchase_order)
+ ((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail))
if self.subcontract_data.order_doctype == "Purchase Order"
- else (se.subcontracting_order == self.subcontracting_order)
+ else (
+ (se.subcontracting_order == self.subcontracting_order)
+ & (se_detail.sco_rm_detail == se_item.sco_rm_detail)
+ )
)
)
- ).run()[0][0]
+ ).run()[0][0] or 0
- if flt(total_supplied, precision) > flt(total_allowed, precision):
+ total_returned = 0
+ if self.subcontract_data.order_doctype == "Subcontracting Order":
+ total_returned = (
+ frappe.qb.from_(se)
+ .inner_join(se_detail)
+ .on(se.name == se_detail.parent)
+ .select(Sum(se_detail.transfer_qty))
+ .where(
+ (se.purpose == "Material Transfer")
+ & (se.docstatus == 1)
+ & (se.is_return == 1)
+ & (se_detail.item_code == se_item.item_code)
+ & (se_detail.sco_rm_detail == se_item.sco_rm_detail)
+ & (se.subcontracting_order == self.subcontracting_order)
+ )
+ ).run()[0][0] or 0
+
+ if flt(total_supplied - total_returned, precision) > flt(total_allowed, precision):
frappe.throw(
_("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format(
se_item.idx,
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index cc8a108bc9..3e0610ef6e 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -449,9 +449,7 @@ class TestStockEntry(FrappeTestCase):
repack.posting_date = nowdate()
repack.posting_time = nowtime()
- expenses_included_in_valuation = frappe.get_value(
- "Company", company, "expenses_included_in_valuation"
- )
+ default_expense_account = frappe.get_value("Company", company, "default_expense_account")
items = get_multiple_items()
repack.items = []
@@ -462,12 +460,12 @@ class TestStockEntry(FrappeTestCase):
"additional_costs",
[
{
- "expense_account": expenses_included_in_valuation,
+ "expense_account": default_expense_account,
"description": "Actual Operating Cost",
"amount": 1000,
},
{
- "expense_account": expenses_included_in_valuation,
+ "expense_account": default_expense_account,
"description": "Additional Operating Cost",
"amount": 200,
},
@@ -506,9 +504,7 @@ class TestStockEntry(FrappeTestCase):
self.check_gl_entries(
"Stock Entry",
repack.name,
- sorted(
- [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]]
- ),
+ sorted([[stock_in_hand_account, 1200, 0.0], ["Cost of Goods Sold - TCP1", 0.0, 1200.0]]),
)
def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 3ca4bad4e4..c1b205132c 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -5,14 +5,16 @@
from datetime import date
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document
-from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
+from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.serial_batch_bundle import SerialBatchBundle
+from erpnext.stock.stock_ledger import get_previous_sle
class StockFreezeError(frappe.ValidationError):
@@ -48,6 +50,69 @@ class StockLedgerEntry(Document):
self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
+ self.validate_inventory_dimension_negative_stock()
+
+ def validate_inventory_dimension_negative_stock(self):
+ extra_cond = ""
+ kwargs = {}
+
+ dimensions = self._get_inventory_dimensions()
+ if not dimensions:
+ return
+
+ for dimension, values in dimensions.items():
+ kwargs[dimension] = values.get("value")
+ extra_cond += f" and {dimension} = %({dimension})s"
+
+ kwargs.update(
+ {
+ "item_code": self.item_code,
+ "warehouse": self.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "company": self.company,
+ }
+ )
+
+ sle = get_previous_sle(kwargs, extra_cond=extra_cond)
+ if sle:
+ flt_precision = cint(frappe.db.get_default("float_precision")) or 2
+ diff = sle.qty_after_transaction + flt(self.actual_qty)
+ diff = flt(diff, flt_precision)
+ if diff < 0 and abs(diff) > 0.0001:
+ self.throw_validation_error(diff, dimensions)
+
+ def throw_validation_error(self, diff, dimensions):
+ dimension_msg = _(", with the inventory {0}: {1}").format(
+ "dimensions" if len(dimensions) > 1 else "dimension",
+ ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()),
+ )
+
+ msg = _(
+ "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction."
+ ).format(
+ abs(diff),
+ frappe.get_desk_link("Item", self.item_code),
+ frappe.get_desk_link("Warehouse", self.warehouse),
+ dimension_msg,
+ self.posting_date,
+ self.posting_time,
+ frappe.get_desk_link(self.voucher_type, self.voucher_no),
+ )
+
+ frappe.throw(msg, title=_("Inventory Dimension Negative Stock"))
+
+ def _get_inventory_dimensions(self):
+ inv_dimensions = get_inventory_dimensions()
+ inv_dimension_dict = {}
+ for dimension in inv_dimensions:
+ if not dimension.get("validate_negative_stock") or not self.get(dimension.fieldname):
+ continue
+
+ dimension["value"] = self.get(dimension.fieldname)
+ inv_dimension_dict.setdefault(dimension.fieldname, dimension)
+
+ return inv_dimension_dict
def on_submit(self):
self.check_stock_frozen_date()
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 5452692a24..b3998b7c7e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -123,13 +123,6 @@ frappe.ui.form.on("Stock Reconciliation", {
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
- "get_query": function() {
- return {
- "filters": {
- "disabled": 0,
- }
- };
- }
},
{
label: __("Ignore Empty Stock"),
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index e36d5769bd..98b4ffdfcf 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -12,6 +12,7 @@ import erpnext
from erpnext.accounts.utils import get_company_default
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_available_serial_nos,
)
@@ -50,6 +51,7 @@ class StockReconciliation(StockController):
self.clean_serial_nos()
self.set_total_qty_and_amount()
self.validate_putaway_capacity()
+ self.validate_inventory_dimension()
if self._action == "submit":
self.validate_reserved_stock()
@@ -57,6 +59,17 @@ class StockReconciliation(StockController):
def on_update(self):
self.set_serial_and_batch_bundle(ignore_validate=True)
+ def validate_inventory_dimension(self):
+ dimensions = get_inventory_dimensions()
+ for dimension in dimensions:
+ for row in self.items:
+ if not row.batch_no and row.current_qty and row.get(dimension.get("fieldname")):
+ frappe.throw(
+ _(
+ "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries."
+ ).format(row.idx, bold(dimension.get("doctype")))
+ )
+
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
@@ -202,8 +215,19 @@ class StockReconciliation(StockController):
self.calculate_difference_amount(item, bundle_data)
return True
+ inventory_dimensions_dict = {}
+ if not item.batch_no and not item.serial_no:
+ for dimension in get_inventory_dimensions():
+ if item.get(dimension.get("fieldname")):
+ inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname"))
+
item_dict = get_stock_balance_for(
- item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no
+ item.item_code,
+ item.warehouse,
+ self.posting_date,
+ self.posting_time,
+ batch_no=item.batch_no,
+ inventory_dimensions_dict=inventory_dimensions_dict,
)
if (item.qty is None or item.qty == item_dict.get("qty")) and (
@@ -507,7 +531,13 @@ class StockReconciliation(StockController):
if not row.batch_no:
data.qty_after_transaction = flt(row.qty, row.precision("qty"))
- if self.docstatus == 2:
+ dimensions = get_inventory_dimensions()
+ has_dimensions = False
+ for dimension in dimensions:
+ if row.get(dimension.get("fieldname")):
+ has_dimensions = True
+
+ if self.docstatus == 2 and (not row.batch_no or not row.serial_and_batch_bundle):
if row.current_qty:
data.actual_qty = -1 * row.current_qty
data.qty_after_transaction = flt(row.current_qty)
@@ -523,6 +553,13 @@ class StockReconciliation(StockController):
data.valuation_rate = flt(row.valuation_rate)
data.stock_value_difference = -1 * flt(row.amount_difference)
+ elif (
+ self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle)
+ ):
+ data.actual_qty = row.qty
+ data.qty_after_transaction = 0.0
+ data.incoming_rate = flt(row.valuation_rate)
+
self.update_inventory_dimensions(row, data)
return data
@@ -911,6 +948,7 @@ def get_stock_balance_for(
posting_time,
batch_no: Optional[str] = None,
with_valuation_rate: bool = True,
+ inventory_dimensions_dict=None,
):
frappe.has_permission("Stock Reconciliation", "write", throw=True)
@@ -939,6 +977,7 @@ def get_stock_balance_for(
posting_time,
with_valuation_rate=with_valuation_rate,
with_serial_no=has_serial_no,
+ inventory_dimensions_dict=inventory_dimensions_dict,
)
if has_serial_no:
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
index c5df319e22..f60a0378b6 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
@@ -92,7 +92,7 @@ frappe.ui.form.on('Stock Reservation Entry', {
'qty', 'read_only', frm.doc.has_serial_no
);
- frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1);
+ frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.from_voucher_type == "Pick List" ? 0 : 1);
},
hide_rate_related_fields(frm) {
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
index 5c3018f734..76cedd4b1e 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -17,8 +17,9 @@
"voucher_no",
"voucher_detail_no",
"column_break_7dxj",
- "against_pick_list",
- "against_pick_list_item",
+ "from_voucher_type",
+ "from_voucher_no",
+ "from_voucher_detail_no",
"section_break_xt4m",
"stock_uom",
"column_break_grdt",
@@ -158,7 +159,7 @@
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"print_width": "150px",
- "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.against_pick_list) || (doc.delivered_qty > 0))",
+ "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.from_voucher_type == \"Pick List\") || (doc.delivered_qty > 0))",
"width": "150px"
},
{
@@ -268,27 +269,7 @@
"label": "Reservation Based On",
"no_copy": 1,
"options": "Qty\nSerial and Batch",
- "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.against_pick_list)"
- },
- {
- "fieldname": "against_pick_list",
- "fieldtype": "Link",
- "label": "Against Pick List",
- "no_copy": 1,
- "options": "Pick List",
- "print_hide": 1,
- "read_only": 1,
- "report_hide": 1,
- "search_index": 1
- },
- {
- "fieldname": "against_pick_list_item",
- "fieldtype": "Data",
- "label": "Against Pick List Item",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1,
- "report_hide": 1
+ "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")"
},
{
"fieldname": "column_break_7dxj",
@@ -297,6 +278,36 @@
{
"fieldname": "column_break_grdt",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "from_voucher_type",
+ "fieldtype": "Select",
+ "label": "From Voucher Type",
+ "no_copy": 1,
+ "options": "\nPick List\nPurchase Receipt",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "from_voucher_detail_no",
+ "fieldtype": "Data",
+ "label": "From Voucher Detail No",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "from_voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "From Voucher No",
+ "no_copy": 1,
+ "options": "from_voucher_type",
+ "print_hide": 1,
+ "read_only": 1,
+ "report_hide": 1,
+ "search_index": 1
}
],
"hide_toolbar": 1,
@@ -304,7 +315,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-08 17:15:13.317706",
+ "modified": "2023-10-19 16:41:16.545416",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reservation Entry",
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index 936be3f73b..6b39965f9b 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+from typing import Literal
+
import frappe
from frappe import _
from frappe.model.document import Document
@@ -113,7 +115,7 @@ class StockReservationEntry(Document):
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
if (
- not self.against_pick_list
+ not self.from_voucher_type
and (self.get("_action") == "submit")
and (self.has_serial_no or self.has_batch_no)
and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch"))
@@ -239,7 +241,7 @@ class StockReservationEntry(Document):
if available_qty_to_reserve <= 0:
msg = _(
- "Row #{0}: Stock not availabe to reserve for Item {1} against Batch {2} in Warehouse {3}."
+ "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}."
).format(
entry.idx,
frappe.bold(self.item_code),
@@ -316,21 +318,24 @@ class StockReservationEntry(Document):
) -> None:
"""Updates total reserved qty in the Pick List."""
- if self.against_pick_list and self.against_pick_list_item:
+ if (
+ self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no
+ ):
sre = frappe.qb.DocType("Stock Reservation Entry")
reserved_qty = (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty))
.where(
(sre.docstatus == 1)
- & (sre.against_pick_list == self.against_pick_list)
- & (sre.against_pick_list_item == self.against_pick_list_item)
+ & (sre.from_voucher_type == "Pick List")
+ & (sre.from_voucher_no == self.from_voucher_no)
+ & (sre.from_voucher_detail_no == self.from_voucher_detail_no)
)
).run(as_list=True)[0][0] or 0
frappe.db.set_value(
"Pick List Item",
- self.against_pick_list_item,
+ self.from_voucher_detail_no,
reserved_qty_field,
reserved_qty,
update_modified=update_modified,
@@ -365,7 +370,7 @@ class StockReservationEntry(Document):
).format(self.status, self.doctype)
frappe.throw(msg)
- if self.against_pick_list:
+ if self.from_voucher_type == "Pick List":
msg = _(
"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one."
)
@@ -761,25 +766,27 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
def create_stock_reservation_entries_for_so_items(
- so: object,
+ sales_order: object,
items_details: list[dict] = None,
- against_pick_list: bool = False,
+ from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
notify=True,
) -> None:
"""Creates Stock Reservation Entries for Sales Order Items."""
from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty
- if not against_pick_list and (
- so.get("_action") == "submit"
- and so.set_warehouse
- and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group"))
+ if not from_voucher_type and (
+ sales_order.get("_action") == "submit"
+ and sales_order.set_warehouse
+ and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group"))
):
return frappe.msgprint(
- _("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse))
+ _("Stock cannot be reserved in the group warehouse {0}.").format(
+ frappe.bold(sales_order.set_warehouse)
+ )
)
- validate_stock_reservation_settings(so)
+ validate_stock_reservation_settings(sales_order)
allow_partial_reservation = frappe.db.get_single_value(
"Stock Settings", "allow_partial_reservation"
@@ -788,38 +795,36 @@ def create_stock_reservation_entries_for_so_items(
items = []
if items_details:
for item in items_details:
- so_item = frappe.get_doc(
- "Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name")
- )
- so_item.reserve_stock = 1
+ so_item = frappe.get_doc("Sales Order Item", item.get("name"))
so_item.warehouse = item.get("warehouse")
so_item.qty_to_reserve = (
- item.get("picked_qty") - item.get("stock_reserved_qty", 0)
- if against_pick_list
- else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1))
+ flt(item.get("qty_to_reserve"))
+ if from_voucher_type in ["Pick List", "Purchase Receipt"]
+ else (
+ flt(item.get("qty_to_reserve"))
+ * (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1)
+ )
)
-
- if against_pick_list:
- so_item.pick_list = item.get("parent")
- so_item.pick_list_item = item.get("name")
- so_item.pick_list_sbb = item.get("serial_and_batch_bundle")
+ so_item.from_voucher_no = item.get("from_voucher_no")
+ so_item.from_voucher_detail_no = item.get("from_voucher_detail_no")
+ so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle")
items.append(so_item)
sre_count = 0
- reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
+ reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name)
- for item in items if items_details else so.get("items"):
+ for item in items if items_details else sales_order.get("items"):
# Skip if `Reserved Stock` is not checked for the item.
if not item.get("reserve_stock"):
continue
# Stock should be reserved from the Pick List if has Picked Qty.
- if not against_pick_list and flt(item.picked_qty) > 0:
+ if not from_voucher_type == "Pick List" and flt(item.picked_qty) > 0:
frappe.throw(
- _(
- "Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List."
- ).format(item.idx, frappe.bold(item.item_code))
+ _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format(
+ item.idx, frappe.bold(item.item_code)
+ )
)
is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value(
@@ -828,13 +833,15 @@ def create_stock_reservation_entries_for_so_items(
# Skip if Non-Stock Item.
if not is_stock_item:
- frappe.msgprint(
- _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
- item.idx, frappe.bold(item.item_code)
- ),
- title=_("Stock Reservation"),
- indicator="yellow",
- )
+ if not from_voucher_type:
+ frappe.msgprint(
+ _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
+ item.idx, frappe.bold(item.item_code)
+ ),
+ title=_("Stock Reservation"),
+ indicator="yellow",
+ )
+
item.db_set("reserve_stock", 0)
continue
@@ -853,13 +860,15 @@ def create_stock_reservation_entries_for_so_items(
# Stock is already reserved for the item, notify the user and skip the item.
if unreserved_qty <= 0:
- frappe.msgprint(
- _("Row #{0}: Stock is already reserved for the Item {1}.").format(
- item.idx, frappe.bold(item.item_code)
- ),
- title=_("Stock Reservation"),
- indicator="yellow",
- )
+ if not from_voucher_type:
+ frappe.msgprint(
+ _("Row #{0}: Stock is already reserved for the Item {1}.").format(
+ item.idx, frappe.bold(item.item_code)
+ ),
+ title=_("Stock Reservation"),
+ indicator="yellow",
+ )
+
continue
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
@@ -867,7 +876,7 @@ def create_stock_reservation_entries_for_so_items(
# No stock available to reserve, notify the user and skip the item.
if available_qty_to_reserve <= 0:
frappe.msgprint(
- _("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format(
+ _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format(
item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
),
title=_("Stock Reservation"),
@@ -893,7 +902,9 @@ def create_stock_reservation_entries_for_so_items(
# Partial Reservation
if qty_to_be_reserved < unreserved_qty:
- if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")):
+ if not from_voucher_type and (
+ not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve"))
+ ):
msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format(
item.idx,
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
@@ -915,33 +926,42 @@ def create_stock_reservation_entries_for_so_items(
sre.warehouse = item.warehouse
sre.has_serial_no = has_serial_no
sre.has_batch_no = has_batch_no
- sre.voucher_type = so.doctype
- sre.voucher_no = so.name
+ sre.voucher_type = sales_order.doctype
+ sre.voucher_no = sales_order.name
sre.voucher_detail_no = item.name
sre.available_qty = available_qty_to_reserve
sre.voucher_qty = item.stock_qty
sre.reserved_qty = qty_to_be_reserved
- sre.company = so.company
+ sre.company = sales_order.company
sre.stock_uom = item.stock_uom
- sre.project = so.project
+ sre.project = sales_order.project
- if against_pick_list:
- sre.against_pick_list = item.pick_list
- sre.against_pick_list_item = item.pick_list_item
+ if from_voucher_type:
+ sre.from_voucher_type = from_voucher_type
+ sre.from_voucher_no = item.from_voucher_no
+ sre.from_voucher_detail_no = item.from_voucher_detail_no
- if item.pick_list_sbb:
- sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb)
- sre.reservation_based_on = "Serial and Batch"
- for entry in sbb.entries:
- sre.append(
- "sb_entries",
- {
- "serial_no": entry.serial_no,
- "batch_no": entry.batch_no,
- "qty": 1 if has_serial_no else abs(entry.qty),
- "warehouse": entry.warehouse,
- },
- )
+ if item.get("serial_and_batch_bundle"):
+ sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
+ sre.reservation_based_on = "Serial and Batch"
+
+ index, picked_qty = 0, 0
+ while index < len(sbb.entries) and picked_qty < qty_to_be_reserved:
+ entry = sbb.entries[index]
+ qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty)
+
+ sre.append(
+ "sb_entries",
+ {
+ "serial_no": entry.serial_no,
+ "batch_no": entry.batch_no,
+ "qty": qty,
+ "warehouse": entry.warehouse,
+ },
+ )
+
+ index += 1
+ picked_qty += qty
sre.save()
sre.submit()
@@ -956,29 +976,37 @@ def cancel_stock_reservation_entries(
voucher_type: str = None,
voucher_no: str = None,
voucher_detail_no: str = None,
- against_pick_list: str = None,
+ from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
+ from_voucher_no: str = None,
+ from_voucher_detail_no: str = None,
sre_list: list[dict] = None,
notify: bool = True,
) -> None:
"""Cancel Stock Reservation Entries."""
- if not sre_list and against_pick_list:
- sre = frappe.qb.DocType("Stock Reservation Entry")
- sre_list = (
- frappe.qb.from_(sre)
- .select(sre.name)
- .where(
- (sre.docstatus == 1)
- & (sre.against_pick_list == against_pick_list)
- & (sre.status.notin(["Delivered", "Cancelled"]))
+ if not sre_list:
+ if voucher_type and voucher_no:
+ sre_list = get_stock_reservation_entries_for_voucher(
+ voucher_type, voucher_no, voucher_detail_no, fields=["name"]
+ )
+ elif from_voucher_type and from_voucher_no:
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ query = (
+ frappe.qb.from_(sre)
+ .select(sre.name)
+ .where(
+ (sre.docstatus == 1)
+ & (sre.from_voucher_type == from_voucher_type)
+ & (sre.from_voucher_no == from_voucher_no)
+ & (sre.status.notin(["Delivered", "Cancelled"]))
+ )
+ .orderby(sre.creation)
)
- .orderby(sre.creation)
- ).run(as_dict=True)
- elif not sre_list and (voucher_type and voucher_no):
- sre_list = get_stock_reservation_entries_for_voucher(
- voucher_type, voucher_no, voucher_detail_no, fields=["name"]
- )
+ if from_voucher_detail_no:
+ query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no)
+
+ sre_list = query.run(as_dict=True)
if sre_list:
for sre in sre_list:
diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
index 1168a4e1c6..f4c74a8aac 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
@@ -5,6 +5,7 @@ from random import randint
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -28,10 +29,6 @@ class TestStockReservationEntry(FrappeTestCase):
items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100
)
- def tearDown(self) -> None:
- cancel_all_stock_reservation_entries()
- return super().tearDown()
-
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_validate_stock_reservation_settings(self) -> None:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
@@ -555,8 +552,9 @@ class TestStockReservationEntry(FrappeTestCase):
(sre.voucher_type == "Sales Order")
& (sre.voucher_no == location.sales_order)
& (sre.voucher_detail_no == location.sales_order_item)
- & (sre.against_pick_list == pl.name)
- & (sre.against_pick_list_item == location.name)
+ & (sre.from_voucher_type == "Pick List")
+ & (sre.from_voucher_no == pl.name)
+ & (sre.from_voucher_detail_no == location.name)
)
).run(as_dict=True)
reserved_sb_details: set[tuple] = {
@@ -567,6 +565,90 @@ class TestStockReservationEntry(FrappeTestCase):
# Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos.
self.assertSetEqual(picked_sb_details, reserved_sb_details)
+ @change_settings(
+ "Stock Settings",
+ {
+ "allow_negative_stock": 0,
+ "enable_stock_reservation": 1,
+ "auto_reserve_serial_and_batch": 1,
+ "pick_serial_and_batch_based_on": "FIFO",
+ "auto_reserve_stock_for_sales_order_on_purchase": 1,
+ },
+ )
+ def test_stock_reservation_from_purchase_receipt(self):
+ from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
+ from erpnext.selling.doctype.sales_order.sales_order import make_material_request
+ from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+
+ items_details = create_items()
+ create_material_receipt(items_details, self.warehouse, qty=10)
+
+ item_list = []
+ for item_code, properties in items_details.items():
+ item_list.append(
+ {
+ "item_code": item_code,
+ "warehouse": self.warehouse,
+ "qty": randint(11, 100),
+ "uom": properties.stock_uom,
+ "rate": randint(10, 400),
+ }
+ )
+
+ so = make_sales_order(
+ item_list=item_list,
+ warehouse=self.warehouse,
+ )
+
+ mr = make_material_request(so.name)
+ mr.schedule_date = today()
+ mr.save().submit()
+
+ po = make_purchase_order(mr.name)
+ po.supplier = "_Test Supplier"
+ po.save().submit()
+
+ pr = make_purchase_receipt(po.name)
+ pr.save().submit()
+
+ for item in pr.items:
+ sre, status, reserved_qty = frappe.db.get_value(
+ "Stock Reservation Entry",
+ {
+ "from_voucher_type": "Purchase Receipt",
+ "from_voucher_no": pr.name,
+ "from_voucher_detail_no": item.name,
+ },
+ ["name", "status", "reserved_qty"],
+ )
+
+ # Test - 1: SRE status should be `Reserved`.
+ self.assertEqual(status, "Reserved")
+
+ # Test - 2: SRE Reserved Qty should be equal to PR Item Qty.
+ self.assertEqual(reserved_qty, item.qty)
+
+ if item.serial_and_batch_bundle:
+ sb_details = frappe.db.get_all(
+ "Serial and Batch Entry",
+ filters={"parent": item.serial_and_batch_bundle},
+ fields=["serial_no", "batch_no", "qty"],
+ as_list=True,
+ )
+ reserved_sb_details = frappe.db.get_all(
+ "Serial and Batch Entry",
+ filters={"parent": sre},
+ fields=["serial_no", "batch_no", "qty"],
+ as_list=True,
+ )
+
+ # Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos.
+ self.assertEqual(set(sb_details), set(reserved_sb_details))
+
+ def tearDown(self) -> None:
+ cancel_all_stock_reservation_entries()
+ return super().tearDown()
+
def create_items() -> dict:
items_properties = [
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 2052daafed..122829032d 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -38,8 +38,8 @@
"stock_reservation_tab",
"enable_stock_reservation",
"column_break_rx3e",
- "auto_reserve_stock_for_sales_order",
"allow_partial_reservation",
+ "auto_reserve_stock_for_sales_order_on_purchase",
"serial_and_batch_reservation_section",
"auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab",
@@ -65,8 +65,7 @@
"stock_frozen_upto_days",
"column_break_26",
"role_allowed_to_create_edit_back_dated_transactions",
- "stock_auth_role",
- "section_break_plhx"
+ "stock_auth_role"
],
"fields": [
{
@@ -356,7 +355,7 @@
{
"default": "1",
"depends_on": "eval: doc.enable_stock_reservation",
- "description": "If enabled,
Partial Stock Reservation Entries can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
+ "description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
"fieldname": "allow_partial_reservation",
"fieldtype": "Check",
"label": "Allow Partial Reservation"
@@ -383,7 +382,7 @@
{
"default": "1",
"depends_on": "eval: doc.enable_stock_reservation",
- "description": "If enabled, Serial and Batch Nos will be auto-reserved based on
Pick Serial / Batch Based On ",
+ "description": "Serial and Batch Nos will be auto-reserved based on
Pick Serial / Batch Based On ",
"fieldname": "auto_reserve_serial_and_batch",
"fieldtype": "Check",
"label": "Auto Reserve Serial and Batch Nos"
@@ -393,14 +392,6 @@
"fieldtype": "Section Break",
"label": "Serial and Batch Reservation"
},
- {
- "default": "0",
- "depends_on": "eval: doc.enable_stock_reservation",
- "description": "If enabled,
Stock Reservation Entries will be created on submission of
Sales Order ",
- "fieldname": "auto_reserve_stock_for_sales_order",
- "fieldtype": "Check",
- "label": "Auto Reserve Stock for Sales Order"
- },
{
"fieldname": "conversion_factor_section",
"fieldtype": "Section Break",
@@ -421,6 +412,14 @@
"fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
"fieldtype": "Check",
"label": "Allow to Edit Stock UOM Qty for Purchase Documents"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.enable_stock_reservation",
+ "description": "Stock will be reserved on submission of
Purchase Receipt created against Material Receipt for Sales Order.",
+ "fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
+ "fieldtype": "Check",
+ "label": "Auto Reserve Stock for Sales Order on Purchase"
}
],
"icon": "icon-cog",
@@ -428,7 +427,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-10-01 14:22:36.136111",
+ "modified": "2023-10-18 12:35:30.068799",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
@@ -453,4 +452,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index a6ab63bb59..e29fc882ce 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -268,7 +268,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if not item:
item = frappe.get_doc("Item", args.get("item_code"))
- if item.variant_of:
+ if item.variant_of and not item.taxes:
item.update_template_tables()
item_defaults = get_item_defaults(item.name, args.company)
@@ -330,8 +330,12 @@ def get_basic_details(args, item, overwrite_warehouse=True):
),
"expense_account": expense_account
or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults),
- "discount_account": get_default_discount_account(args, item_defaults),
- "provisional_expense_account": get_provisional_account(args, item_defaults),
+ "discount_account": get_default_discount_account(
+ args, item_defaults, item_group_defaults, brand_defaults
+ ),
+ "provisional_expense_account": get_provisional_account(
+ args, item_defaults, item_group_defaults, brand_defaults
+ ),
"cost_center": get_default_cost_center(
args, item_defaults, item_group_defaults, brand_defaults
),
@@ -606,6 +610,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
# all templates have validity and no template is valid
if not taxes_with_validity and (not taxes_with_no_validity):
+ out["item_tax_template"] = ""
return None
# do not change if already a valid template
@@ -685,12 +690,22 @@ def get_default_expense_account(args, item, item_group, brand):
)
-def get_provisional_account(args, item):
- return item.get("default_provisional_account") or args.default_provisional_account
+def get_provisional_account(args, item, item_group, brand):
+ return (
+ item.get("default_provisional_account")
+ or item_group.get("default_provisional_account")
+ or brand.get("default_provisional_account")
+ or args.default_provisional_account
+ )
-def get_default_discount_account(args, item):
- return item.get("default_discount_account") or args.discount_account
+def get_default_discount_account(args, item, item_group, brand):
+ return (
+ item.get("default_discount_account")
+ or item_group.get("default_discount_account")
+ or brand.get("default_discount_account")
+ or args.discount_account
+ )
def get_default_deferred_account(args, item, fieldname=None):
@@ -737,6 +752,12 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
data = frappe.get_attr(path)(args.get("item_code"), company)
if data and (data.selling_cost_center or data.buying_cost_center):
+ if args.get("customer") and data.selling_cost_center:
+ return data.selling_cost_center
+
+ elif args.get("supplier") and data.buying_cost_center:
+ return data.buying_cost_center
+
return data.selling_cost_center or data.buying_cost_center
if not cost_center and args.get("cost_center"):
diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.js b/erpnext/stock/report/reserved_stock/reserved_stock.js
index 2199f52df0..68727411d5 100644
--- a/erpnext/stock/report/reserved_stock/reserved_stock.js
+++ b/erpnext/stock/report/reserved_stock/reserved_stock.js
@@ -91,16 +91,30 @@ frappe.query_reports["Reserved Stock"] = {
},
},
{
- fieldname: "against_pick_list",
- label: __("Against Pick List"),
+ fieldname: "from_voucher_type",
+ label: __("From Voucher Type"),
fieldtype: "Link",
- options: "Pick List",
+ options: "DocType",
+ get_query: () => ({
+ filters: {
+ name: ["in", ["Pick List", "Purchase Receipt"]],
+ }
+ }),
+ },
+ {
+ fieldname: "from_voucher_no",
+ label: __("From Voucher No"),
+ fieldtype: "Dynamic Link",
+ options: "from_voucher_type",
get_query: () => ({
filters: {
docstatus: 1,
company: frappe.query_report.get_filter_value("company"),
},
}),
+ get_options: function () {
+ return frappe.query_report.get_filter_value("from_voucher_type");
+ },
},
{
fieldname: "reservation_based_on",
diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.py b/erpnext/stock/report/reserved_stock/reserved_stock.py
index d93ee1c88f..21ce203ad6 100644
--- a/erpnext/stock/report/reserved_stock/reserved_stock.py
+++ b/erpnext/stock/report/reserved_stock/reserved_stock.py
@@ -44,7 +44,8 @@ def get_data(filters):
(sre.available_qty - sre.reserved_qty).as_("available_qty"),
sre.voucher_type,
sre.voucher_no,
- sre.against_pick_list,
+ sre.from_voucher_type,
+ sre.from_voucher_no,
sre.name.as_("stock_reservation_entry"),
sre.status,
sre.project,
@@ -65,7 +66,8 @@ def get_data(filters):
"warehouse",
"voucher_type",
"voucher_no",
- "against_pick_list",
+ "from_voucher_type",
+ "from_voucher_no",
"reservation_based_on",
"status",
"project",
@@ -142,7 +144,6 @@ def get_columns():
"fieldname": "voucher_type",
"label": _("Voucher Type"),
"fieldtype": "Data",
- "options": "Warehouse",
"width": 110,
},
{
@@ -153,11 +154,17 @@ def get_columns():
"width": 120,
},
{
- "fieldname": "against_pick_list",
- "label": _("Against Pick List"),
- "fieldtype": "Link",
- "options": "Pick List",
- "width": 130,
+ "fieldname": "from_voucher_type",
+ "label": _("From Voucher Type"),
+ "fieldtype": "Data",
+ "width": 110,
+ },
+ {
+ "fieldname": "from_voucher_no",
+ "label": _("From Voucher No"),
+ "fieldtype": "Dynamic Link",
+ "options": "from_voucher_type",
+ "width": 120,
},
{
"fieldname": "stock_reservation_entry",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index d3807b0f97..b950f18810 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import copy
+import gzip
import json
from typing import Optional, Set, Tuple
@@ -10,20 +11,11 @@ from frappe import _, scrub
from frappe.model.meta import get_field_precision
from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum
-from frappe.utils import (
- cint,
- flt,
- get_link_to_form,
- getdate,
- gzip_compress,
- gzip_decompress,
- now,
- nowdate,
- parse_json,
-)
+from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
+from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
)
@@ -294,7 +286,7 @@ def get_reposting_data(file_path) -> dict:
attached_file = frappe.get_doc("File", file_name)
- data = gzip_decompress(attached_file.get_content())
+ data = gzip.decompress(attached_file.get_content())
if data := json.loads(data.decode("utf-8")):
data = data
@@ -377,7 +369,7 @@ def get_reposting_file_name(dt, dn):
def create_json_gz_file(data, doc, file_name=None) -> str:
encoded_content = frappe.safe_encode(frappe.as_json(data))
- compressed_content = gzip_compress(encoded_content)
+ compressed_content = gzip.compress(encoded_content)
if not file_name:
json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
@@ -711,10 +703,17 @@ class update_entries_after(object):
):
sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
+ dimensions = get_inventory_dimensions()
+ has_dimensions = False
+ if dimensions:
+ for dimension in dimensions:
+ if sle.get(dimension.get("fieldname")):
+ has_dimensions = True
+
if sle.serial_and_batch_bundle:
self.calculate_valuation_for_serial_batch_bundle(sle)
else:
- if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
+ if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
# assert
self.wh_data.valuation_rate = sle.valuation_rate
self.wh_data.qty_after_transaction = sle.qty_after_transaction
@@ -1297,7 +1296,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
return sle[0] if sle else frappe._dict()
-def get_previous_sle(args, for_update=False):
+def get_previous_sle(args, for_update=False, extra_cond=None):
"""
get the last sle on or before the current time-bucket,
to get actual qty before transaction, this function
@@ -1312,7 +1311,9 @@ def get_previous_sle(args, for_update=False):
}
"""
args["name"] = args.get("sle", None) or ""
- sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
+ sle = get_stock_ledger_entries(
+ args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
+ )
return sle and sle[0] or {}
@@ -1324,6 +1325,7 @@ def get_stock_ledger_entries(
for_update=False,
debug=False,
check_serial_no=True,
+ extra_cond=None,
):
"""get stock ledger entries filtered by specific posting datetime conditions"""
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
@@ -1361,6 +1363,9 @@ def get_stock_ledger_entries(
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
+ if extra_cond:
+ conditions += f"{extra_cond}"
+
return frappe.db.sql(
"""
select *, timestamp(posting_date, posting_time) as "timestamp"
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 02444064c1..bd0d4697c9 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -95,6 +95,7 @@ def get_stock_balance(
posting_time=None,
with_valuation_rate=False,
with_serial_no=False,
+ inventory_dimensions_dict=None,
):
"""Returns stock balance quantity at given warehouse on given posting date or current date.
@@ -114,7 +115,13 @@ def get_stock_balance(
"posting_time": posting_time,
}
- last_entry = get_previous_sle(args)
+ extra_cond = ""
+ if inventory_dimensions_dict:
+ for field, value in inventory_dimensions_dict.items():
+ args[field] = value
+ extra_cond += f" and {field} = %({field})s"
+
+ last_entry = get_previous_sle(args, extra_cond=extra_cond)
if with_valuation_rate:
if with_serial_no:
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
index f2b395ac10..587a3b4ebf 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', {
get_materials_from_supplier: function (frm) {
let sco_rm_details = [];
- if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) {
+ if (frm.doc.status != "Closed" && frm.doc.supplied_items) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) {
sco_rm_details.push(d.name);
@@ -193,7 +193,7 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
}
has_unsupplied_items() {
- return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
+ return this.frm.doc['supplied_items'].some(item => item.required_qty > (item.supplied_qty - item.returned_qty));
}
make_subcontracting_receipt() {
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 6aecaf98a5..7e06444e1e 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -410,7 +410,6 @@ class SubcontractingReceipt(SubcontractingController):
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
- expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
warehouse_with_no_account = []
@@ -482,10 +481,7 @@ class SubcontractingReceipt(SubcontractingController):
divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount"))
if divisional_loss:
- if self.is_return:
- loss_account = expenses_included_in_valuation
- else:
- loss_account = item.expense_account
+ loss_account = item.expense_account
self.add_gl_entry(
gl_entries=gl_entries,
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index d4daacd4ea..f96823b290 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -1,13 +1,6 @@
frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
- frm.set_query("customer", function () {
- return {
- filters: {
- "disabled": 0
- }
- };
- });
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html
deleted file mode 100644
index 358c1c52e5..0000000000
--- a/erpnext/templates/generators/item/item.html
+++ /dev/null
@@ -1,80 +0,0 @@
-{% extends "templates/web.html" %}
-{% from "erpnext/templates/includes/macros.html" import recommended_item_row %}
-
-{% block title %} {{ title }} {% endblock %}
-
-{% block breadcrumbs %}
-
- {% include "templates/includes/breadcrumbs.html" %}
-
-{% endblock %}
-
-{% block page_content %}
-
- {% from "erpnext/templates/includes/macros.html" import product_image %}
-
-
-
-
- {% include "templates/generators/item/item_image.html" %}
- {% include "templates/generators/item/item_details.html" %}
-
-
-
-
-
-
-
- {% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %}
- {% set info_col = 'col-9' if show_recommended_items else 'col-12' %}
-
- {% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %}
-
-
-
-
-
- {% if show_tabs and tabs %}
-
-
- {{ web_block("Section with Tabs", values=tabs, add_container=0,
- add_top_padding=0, add_bottom_padding=0)
- }}
-
- {% elif website_specifications %}
- {% include "templates/generators/item/item_specifications.html"%}
- {% endif %}
-
-
- {{ doc.website_content or '' }}
-
-
- {% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %}
- {% include "templates/generators/item/item_reviews.html"%}
- {% endif %}
-
-
-
-
-
- {% if show_recommended_items %}
-
-
-
- {% for item in recommended_items %}
- {{ recommended_item_row(item) }}
- {% endfor %}
-
-
- {% endif %}
-
-
-{% endblock %}
-
-{% block base_scripts %}
-
-
-{{ include_script("frappe-web.bundle.js") }}
-{{ include_script("controls.bundle.js") }}
-{{ include_script("dialog.bundle.js") }}
-{% endblock %}
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
deleted file mode 100644
index 9bd3f7514c..0000000000
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ /dev/null
@@ -1,180 +0,0 @@
-{% if shopping_cart and shopping_cart.cart_settings.enabled %}
-
-{% set cart_settings = shopping_cart.cart_settings %}
-{% set product_info = shopping_cart.product_info %}
-
-
-
-
- {% if cart_settings.show_price and product_info.price %}
- {% set price_info = product_info.price %}
-
-
-
-
- {{ price_info.formatted_price_sales_uom }}
- {{ price_info.currency }}
-
-
-
- {% if price_info.formatted_mrp %}
-
- MRP {{ price_info.formatted_mrp }}
-
-
- -{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}}
-
- {% endif %}
-
-
-
- ({{ price_info.formatted_price }} / {{ product_info.uom }})
-
-
- {% else %}
- {{ _("UOM") }} : {{ product_info.uom }}
- {% endif %}
-
- {% if cart_settings.show_stock_availability %}
-
- {% if product_info.get("on_backorder") %}
-
- {{ _('Available on backorder') }}
-
- {% elif product_info.in_stock == 0 %}
-
- {{ _('Out of stock') }}
-
- {% elif product_info.in_stock == 1 %}
-
- {{ _('In stock') }}
- {% if product_info.show_stock_qty and product_info.stock_qty %}
- ({{ product_info.stock_qty }})
- {% endif %}
-
- {% endif %}
-
- {% endif %}
-
-
- {% if doc.offers %}
-
-
-
-
-
- Available Offers
-
-
- {% for offer in doc.offers %}
-
-
-
- {{ _(offer.offer_title) }}:
- {{ _(offer.offer_subtitle) if offer.offer_subtitle else '' }}
-
- {{ _("More") }}
-
-
-
- {% endfor %}
-
- {% endif %}
-
-
-
-
-
- {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
-
- {{ _("View in Cart") if cart_settings.enable_checkout else _("View in Quote") }}
-
-
-
-
-
-
-
- {{ _("Add to Cart") if cart_settings.enable_checkout else _("Add to Quote") }}
-
- {% endif %}
-
-
- {% if cart_settings.show_contact_us_button %}
- {% include "templates/generators/item/item_inquiry.html" %}
- {% endif %}
-
-
-
-
-
-
-
-{% endif %}
diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html
deleted file mode 100644
index e97a275fbd..0000000000
--- a/erpnext/templates/generators/item/item_configure.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% if shopping_cart and shopping_cart.cart_settings.enabled %}
-{% set cart_settings = shopping_cart.cart_settings %}
-
-
- {% if cart_settings.enable_variants | int %}
-
- {{ _('Select Variant') }}
-
- {% endif %}
- {% if cart_settings.show_contact_us_button %}
- {% include "templates/generators/item/item_inquiry.html" %}
- {% endif %}
-
-
-{% endif %}
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
deleted file mode 100644
index 9beba3fd01..0000000000
--- a/erpnext/templates/generators/item/item_configure.js
+++ /dev/null
@@ -1,343 +0,0 @@
-class ItemConfigure {
- constructor(item_code, item_name) {
- this.item_code = item_code;
- this.item_name = item_name;
-
- this.get_attributes_and_values()
- .then(attribute_data => {
- this.attribute_data = attribute_data;
- this.show_configure_dialog();
- });
- }
-
- show_configure_dialog() {
- const fields = this.attribute_data.map(a => {
- return {
- fieldtype: 'Select',
- label: a.attribute,
- fieldname: a.attribute,
- options: a.values.map(v => {
- return {
- label: v,
- value: v
- };
- }),
- change: (e) => {
- this.on_attribute_selection(e);
- }
- };
- });
-
- this.dialog = new frappe.ui.Dialog({
- title: __('Select Variant for {0}', [this.item_name]),
- fields,
- on_hide: () => {
- set_continue_configuration();
- }
- });
-
- this.attribute_data.forEach(a => {
- const field = this.dialog.get_field(a.attribute);
- const $a = $(`
${__("Clear")} `);
- $a.on('click', (e) => {
- e.preventDefault();
- this.dialog.set_value(a.attribute, '');
- });
- field.$wrapper.find('.help-box').append($a);
- });
-
- this.append_status_area();
- this.dialog.show();
-
- this.dialog.set_values(JSON.parse(localStorage.getItem(this.get_cache_key())));
-
- $('.btn-configure').prop('disabled', false);
- }
-
- on_attribute_selection(e) {
- if (e) {
- const changed_fieldname = $(e.target).data('fieldname');
- this.show_range_input_if_applicable(changed_fieldname);
- } else {
- this.show_range_input_for_all_fields();
- }
-
- const values = this.dialog.get_values();
- if (Object.keys(values).length === 0) {
- this.clear_status();
- localStorage.removeItem(this.get_cache_key());
- return;
- }
-
- // save state
- localStorage.setItem(this.get_cache_key(), JSON.stringify(values));
-
- // show
- this.set_loading_status();
-
- this.get_next_attribute_and_values(values)
- .then(data => {
- const {
- valid_options_for_attributes,
- } = data;
-
- this.set_item_found_status(data);
-
- for (let attribute in valid_options_for_attributes) {
- const valid_options = valid_options_for_attributes[attribute];
- const options = this.dialog.get_field(attribute).df.options;
- const new_options = options.map(o => {
- o.disabled = !valid_options.includes(o.value);
- return o;
- });
-
- this.dialog.set_df_property(attribute, 'options', new_options);
- this.dialog.get_field(attribute).set_options();
- }
- });
- }
-
- show_range_input_for_all_fields() {
- this.dialog.fields.forEach(f => {
- this.show_range_input_if_applicable(f.fieldname);
- });
- }
-
- show_range_input_if_applicable(fieldname) {
- const changed_field = this.dialog.get_field(fieldname);
- const changed_value = changed_field.get_value();
- if (changed_value && changed_value.includes(' to ')) {
- // possible range input
- let numbers = changed_value.split(' to ');
- numbers = numbers.map(number => parseFloat(number));
-
- if (!numbers.some(n => isNaN(n))) {
- numbers.sort((a, b) => a - b);
- if (changed_field.$input_wrapper.find('.range-selector').length) {
- return;
- }
- const parent = $('
')
- .insertBefore(changed_field.$input_wrapper.find('.help-box'));
- const control = frappe.ui.form.make_control({
- df: {
- fieldtype: 'Int',
- label: __('Enter value betweeen {0} and {1}', [numbers[0], numbers[1]]),
- change: () => {
- const value = control.get_value();
- if (value < numbers[0] || value > numbers[1]) {
- control.$wrapper.addClass('was-validated');
- control.set_description(
- __('Value must be between {0} and {1}', [numbers[0], numbers[1]]));
- control.$input[0].setCustomValidity('error');
- } else {
- control.$wrapper.removeClass('was-validated');
- control.set_description('');
- control.$input[0].setCustomValidity('');
- this.update_range_values(fieldname, value);
- }
- }
- },
- render_input: true,
- parent
- });
- control.$wrapper.addClass('mt-3');
- }
- }
- }
-
- update_range_values(attribute, range_value) {
- this.range_values = this.range_values || {};
- this.range_values[attribute] = range_value;
- }
-
- show_remaining_optional_attributes() {
- // show all attributes if remaining
- // unselected attributes are all optional
- const unselected_attributes = this.dialog.fields.filter(df => {
- const value_selected = this.dialog.get_value(df.fieldname);
- return !value_selected;
- });
- const is_optional_attribute = df => {
- const optional_attributes = this.attribute_data
- .filter(a => a.optional).map(a => a.attribute);
- return optional_attributes.includes(df.fieldname);
- };
- if (unselected_attributes.every(is_optional_attribute)) {
- unselected_attributes.forEach(df => {
- this.dialog.fields_dict[df.fieldname].$wrapper.show();
- });
- }
- }
-
- set_loading_status() {
- this.dialog.$status_area.html(`
-
- ${__('Loading...')}
-
- `);
- }
-
- set_item_found_status(data) {
- const html = this.get_html_for_item_found(data);
- this.dialog.$status_area.html(html);
- }
-
- clear_status() {
- this.dialog.$status_area.empty();
- }
-
- get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, available_qty, settings }) {
- const one_item = exact_match.length === 1
- ? exact_match[0]
- : filtered_items_count === 1
- ? filtered_items[0]
- : '';
-
- let item_add_to_cart = one_item ? `
-
-
- ${frappe.utils.icon('assets', 'md')}
-
- ${__("Add to Cart")}
-
- ` : '';
-
- const items_found = filtered_items_count === 1 ?
- __('{0} item found.', [filtered_items_count]) :
- __('{0} items found.', [filtered_items_count]);
-
- /* eslint-disable indent */
- const item_found_status = exact_match.length === 1
- ? `
-
- ${one_item}
- ${product_info && product_info.price && !$.isEmptyObject(product_info.price)
- ? '(' + product_info.price.formatted_price_sales_uom + ')'
- : ''
- }
-
- ${available_qty === 0 && product_info && product_info?.is_stock_item
- ? '(' + __('Out of Stock') + ') ' : ''}
-
-
-
- ${__('Clear Values')}
-
-
`
- : `
-
- ${items_found}
-
-
- ${__('Clear values')}
-
-
`;
- /* eslint-disable indent */
-
- if (!product_info?.allow_items_not_in_stock && available_qty === 0
- && product_info && product_info?.is_stock_item) {
- item_add_to_cart = '';
- }
-
- return `
- ${item_found_status}
- ${item_add_to_cart}
- `;
- }
-
- btn_add_to_cart(e) {
- if (frappe.session.user !== 'Guest') {
- localStorage.removeItem(this.get_cache_key());
- }
- const item_code = $(e.currentTarget).data('item-code');
- const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
- return `${attribute}: ${this.range_values[attribute]}`;
- }).join('\n');
- erpnext.e_commerce.shopping_cart.update_cart({
- item_code,
- additional_notes,
- qty: 1
- });
- this.dialog.hide();
- }
-
- btn_clear_values() {
- this.dialog.fields_list.forEach(f => {
- if (f.df?.options) {
- f.df.options = f.df.options.map(option => {
- option.disabled = false;
- return option;
- });
- }
- });
- this.dialog.clear();
- this.dialog.$status_area.empty();
- this.on_attribute_selection();
- }
-
- append_status_area() {
- this.dialog.$status_area = $('
');
- this.dialog.$wrapper.find('.modal-body').append(this.dialog.$status_area);
- this.dialog.$wrapper.on('click', '[data-action]', (e) => {
- e.preventDefault();
- const $target = $(e.currentTarget);
- const action = $target.data('action');
- const method = this[action];
- method.call(this, e);
- });
- this.dialog.$wrapper.addClass('item-configurator-dialog');
- }
-
- get_next_attribute_and_values(selected_attributes) {
- return this.call('erpnext.e_commerce.variant_selector.utils.get_next_attribute_and_values', {
- item_code: this.item_code,
- selected_attributes
- });
- }
-
- get_attributes_and_values() {
- return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', {
- item_code: this.item_code
- });
- }
-
- get_cache_key() {
- return `configure:${this.item_code}`;
- }
-
- call(method, args) {
- // promisified frappe.call
- return new Promise((resolve, reject) => {
- frappe.call(method, args)
- .then(r => resolve(r.message))
- .fail(reject);
- });
- }
-}
-
-function set_continue_configuration() {
- const $btn_configure = $('.btn-configure');
- const { itemCode } = $btn_configure.data();
-
- if (localStorage.getItem(`configure:${itemCode}`)) {
- $btn_configure.text(__('Continue Selection'));
- } else {
- $btn_configure.text(__('Select Variant'));
- }
-}
-
-frappe.ready(() => {
- const $btn_configure = $('.btn-configure');
- if (!$btn_configure.length) return;
- const { itemCode, itemName } = $btn_configure.data();
-
- set_continue_configuration();
-
- $btn_configure.on('click', () => {
- $btn_configure.prop('disabled', true);
- new ItemConfigure(itemCode, itemName);
- });
-});
diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html
deleted file mode 100644
index 028936bf5f..0000000000
--- a/erpnext/templates/generators/item/item_details.html
+++ /dev/null
@@ -1,63 +0,0 @@
-{% set width_class = "expand" if not slides else "" %}
-{% set cart_settings = shopping_cart.cart_settings %}
-{% set product_info = shopping_cart.product_info %}
-{% set price_info = product_info.get('price') or {} %}
-
-
-
-
-
- {{ doc.web_item_name }}
-
-
-
- {% if cart_settings.enable_wishlist %}
-
-
-
-
-
- {% endif %}
-
-
-
-
- {{ _(doc.item_group) }}
-
-
- {{ _("Item Code") }}:
-
- {{ doc.item_code }}
-
- {% if has_variants %}
-
- {% include "templates/generators/item/item_configure.html" %}
- {% else %}
-
- {% include "templates/generators/item/item_add_to_cart.html" %}
- {% endif %}
-
-
- {% if frappe.utils.strip_html(doc.web_long_description or '') %}
- {{ doc.web_long_description | safe }}
- {% elif frappe.utils.strip_html(doc.description or '') %}
- {{ doc.description | safe }}
- {% else %}
- {{ "" }}
- {% endif %}
-
-
-
-{% block base_scripts %}
-
-
-{% endblock %}
-
-
\ No newline at end of file
diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html
deleted file mode 100644
index e1bb3b9865..0000000000
--- a/erpnext/templates/generators/item/item_image.html
+++ /dev/null
@@ -1,108 +0,0 @@
-{% set column_size = 5 if slides else 4 %}
-
- {% if slides %}
-
- {% for item in slides %}
-
- {% endfor %}
-
- {{ product_image(slides[0].image, 'product-image') }}
-
-
- {% else %}
- {{ product_image(doc.website_image, alt=doc.website_image_alt or doc.item_name) }}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html
deleted file mode 100644
index af636f1582..0000000000
--- a/erpnext/templates/generators/item/item_inquiry.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% if shopping_cart and shopping_cart.cart_settings.enabled %}
-{% set cart_settings = shopping_cart.cart_settings %}
- {% if cart_settings.show_contact_us_button | int %}
-
- {{ _('Contact Us') }}
-
- {% endif %}
-
-{% endif %}
diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js
deleted file mode 100644
index 0aee996672..0000000000
--- a/erpnext/templates/generators/item/item_inquiry.js
+++ /dev/null
@@ -1,77 +0,0 @@
-frappe.ready(() => {
- const d = new frappe.ui.Dialog({
- title: __('Contact Us'),
- fields: [
- {
- fieldtype: 'Data',
- label: __('Full Name'),
- fieldname: 'lead_name',
- reqd: 1
- },
- {
- fieldtype: 'Data',
- label: __('Organization Name'),
- fieldname: 'company_name',
- },
- {
- fieldtype: 'Data',
- label: __('Email'),
- fieldname: 'email_id',
- options: 'Email',
- reqd: 1
- },
- {
- fieldtype: 'Data',
- label: __('Phone Number'),
- fieldname: 'phone',
- options: 'Phone',
- reqd: 1
- },
- {
- fieldtype: 'Data',
- label: __('Subject'),
- fieldname: 'subject',
- reqd: 1
- },
- {
- fieldtype: 'Text',
- label: __('Message'),
- fieldname: 'message',
- reqd: 1
- }
- ],
- primary_action: send_inquiry,
- primary_action_label: __('Send')
- });
-
- function send_inquiry() {
- const values = d.get_values();
- const doc = Object.assign({}, values);
- delete doc.subject;
- delete doc.message;
-
- d.hide();
-
- frappe.call('erpnext.e_commerce.shopping_cart.cart.create_lead_for_item_inquiry', {
- lead: doc,
- subject: values.subject,
- message: values.message
- }).then(r => {
- if (r.message) {
- d.clear();
- }
- });
- }
-
- $('.btn-inquiry').click((e) => {
- const $btn = $(e.target);
- const item_code = $btn.data('item-code');
- d.set_value('subject', 'Inquiry about ' + item_code);
- if (!['Administrator', 'Guest'].includes(frappe.session.user)) {
- d.set_value('email_id', frappe.session.user);
- d.set_value('lead_name', frappe.get_cookie('full_name'));
- }
-
- d.show();
- });
-});
diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html
deleted file mode 100644
index c62c6f7749..0000000000
--- a/erpnext/templates/generators/item/item_reviews.html
+++ /dev/null
@@ -1,88 +0,0 @@
-{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
-
-
-
-
-
-
-
-
- {% if frappe.session.user != "Guest" and user_is_customer %}
-
- {{ _("Write a Review") }}
-
- {% endif %}
-
-
-
-
- {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
-
-
-
-
- {% if reviews %}
- {{ user_review(reviews) }}
-
- {% if total_reviews > 4 %}
-
- {% endif %}
-
- {% else %}
-
- {{ _("No Reviews") }}
-
- {% endif %}
-
-
-
-
diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html
deleted file mode 100644
index 0814d81c8a..0000000000
--- a/erpnext/templates/generators/item/item_specifications.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-{% if website_specifications %}
-
-
- {% if not show_tabs %}
-
- Product Details
-
- {% endif %}
-
- {% for d in website_specifications -%}
-
- {{ d.label }}
- {{ d.description }}
-
- {%- endfor %}
-
-
-
-{% endif %}
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
deleted file mode 100644
index 956c3c51e6..0000000000
--- a/erpnext/templates/generators/item_group.html
+++ /dev/null
@@ -1,72 +0,0 @@
-{% from "erpnext/templates/includes/macros.html" import field_filter_section, attribute_filter_section, discount_range_filters %}
-{% extends "templates/web.html" %}
-
-{% block header %}
-
{{ _(item_group_name) }}
-{% endblock header %}
-
-{% block script %}
-
-{% endblock %}
-
-{% block breadcrumbs %}
-
- {% include "templates/includes/breadcrumbs.html" %}
-
-{% endblock %}
-
-{% block page_content %}
-
-
- {% if slideshow %}
- {{ web_block(
- "Hero Slider",
- values=slideshow,
- add_container=0,
- add_top_padding=0,
- add_bottom_padding=0,
- ) }}
- {% endif %}
-
- {% if description %}
-
{{ description or ""}}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
{{ _('Filters') }}
-
{{ _('Clear All') }}
-
-
- {{ field_filter_section(field_filters) }}
-
-
- {{ attribute_filter_section(attribute_filters) }}
-
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html
deleted file mode 100644
index 830ed649f5..0000000000
--- a/erpnext/templates/includes/cart/address_card.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
diff --git a/erpnext/templates/includes/cart/address_picker_card.html b/erpnext/templates/includes/cart/address_picker_card.html
deleted file mode 100644
index 646210e65f..0000000000
--- a/erpnext/templates/includes/cart/address_picker_card.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
{{ address.title }}
-
- {{ address.display }}
-
-
{{ _('Edit') }}
-
-
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
deleted file mode 100644
index a8188ec825..0000000000
--- a/erpnext/templates/includes/cart/cart_address.html
+++ /dev/null
@@ -1,189 +0,0 @@
-{% from "erpnext/templates/includes/cart/cart_macros.html" import show_address %}
-
-{% if addresses | length == 1%}
- {% set select_address = True %}
-{% endif %}
-
-
-
-
-
- {% for address in shipping_addresses %}
- {% if doc.shipping_address_name == address.name %}
-
-
- {% include "templates/includes/cart/address_card.html" %}
-
-
- {% endif %}
- {% endfor %}
-
-
-
-
-
-
- {{ _('Billing Address is same as Shipping Address') }}
-
-
-
-{% if billing_addresses %}
-
-
-
-
- {% for address in billing_addresses %}
- {% if doc.customer_address == address.name %}
-
-
- {% include "templates/includes/cart/address_card.html" %}
-
-
- {% endif %}
- {% endfor %}
-
-{% endif %}
-
-
diff --git a/erpnext/templates/includes/cart/cart_address_picker.html b/erpnext/templates/includes/cart/cart_address_picker.html
deleted file mode 100644
index 66a50ecc9f..0000000000
--- a/erpnext/templates/includes/cart/cart_address_picker.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
{{ _("Shipping Address") }}
-
diff --git a/erpnext/templates/includes/cart/cart_dropdown.html b/erpnext/templates/includes/cart/cart_dropdown.html
deleted file mode 100644
index 38ad183916..0000000000
--- a/erpnext/templates/includes/cart/cart_dropdown.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
- {% if doc.items %}
-
-
- {% include "templates/includes/cart/cart_items_dropdown.html" %}
-
-
- {% else %}
-
{{ _("Cart is Empty") }}
- {% endif %}
-
diff --git a/erpnext/templates/includes/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html
deleted file mode 100644
index 428b36e9b3..0000000000
--- a/erpnext/templates/includes/cart/cart_items.html
+++ /dev/null
@@ -1,113 +0,0 @@
-{% from "erpnext/templates/includes/macros.html" import product_image %}
-
-{% macro item_subtotal(item) %}
-
- {{ item.get_formatted('amount') }}
-
-
- {% if item.is_free_item %}
-
-
- {{ _('FREE') }}
-
-
- {% else %}
-
- {{ _('Rate:') }} {{ item.get_formatted('rate') }}
-
- {% endif %}
-{% endmacro %}
-
-{% for d in doc.items %}
-
-
-
-
- {% if d.thumbnail %}
- {{ product_image(d.thumbnail, alt="d.web_item_name", no_border=True) }}
- {% else %}
-
- {{ frappe.utils.get_abbr(d.web_item_name) or "NA" }}
-
- {% endif %}
-
-
-
-
- {{ d.get("web_item_name") or d.item_name }}
-
-
- {{ d.item_code }}
-
- {%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
- {% if variant_of %}
-
- {{ _('Variant of') }}
-
- {{ variant_of }}
-
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
- {% set disabled = 'disabled' if d.is_free_item else '' %}
-
-
-
- {{ '–' if not d.is_free_item else ''}}
-
-
-
-
-
-
-
- {{ '+' if not d.is_free_item else ''}}
-
-
-
-
-
- {% if not d.is_free_item %}
-
- {% endif %}
-
-
-
-
-
- {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
-
- {{ item_subtotal(d) }}
-
- {% endif %}
-
-
-
- {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
-
- {{ item_subtotal(d) }}
-
- {% endif %}
-
-{% endfor %}
diff --git a/erpnext/templates/includes/cart/cart_items_dropdown.html b/erpnext/templates/includes/cart/cart_items_dropdown.html
deleted file mode 100644
index 5d107fc0d0..0000000000
--- a/erpnext/templates/includes/cart/cart_items_dropdown.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description_cart %}
-
-{% for d in doc.items %}
-
-
- {{ item_name_and_description_cart(d) }}
-
-
- {{ d.get_formatted("amount") }}
-
-
-{% endfor %}
diff --git a/erpnext/templates/includes/cart/cart_items_total.html b/erpnext/templates/includes/cart/cart_items_total.html
deleted file mode 100644
index c94fde462b..0000000000
--- a/erpnext/templates/includes/cart/cart_items_total.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- {{ _("Total") }}
-
-
- {{ doc.get_formatted("total") }}
-
-
\ No newline at end of file
diff --git a/erpnext/templates/includes/cart/cart_macros.html b/erpnext/templates/includes/cart/cart_macros.html
deleted file mode 100644
index fd95dba424..0000000000
--- a/erpnext/templates/includes/cart/cart_macros.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% macro show_address(address, doc, fieldname, select_address=False) %}
-{% set selected=address.name==doc.get(fieldname) %}
-
-
-
-
-
- {{ address.name }}
-
-
-
-
-
-
{{ address.display }}
-
-
-{% endmacro %}
diff --git a/erpnext/templates/includes/cart/cart_payment_summary.html b/erpnext/templates/includes/cart/cart_payment_summary.html
deleted file mode 100644
index b5655a237b..0000000000
--- a/erpnext/templates/includes/cart/cart_payment_summary.html
+++ /dev/null
@@ -1,84 +0,0 @@
-
-{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
-
- {{ _("Payment Summary") }}
-
-{% endif %}
-
-
-
- {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
-
-
- {% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}
- {{ _("Net Total (") + total_items + _(" Items)") }}
- {{ doc.get_formatted("net_total") }}
-
-
-
- {% for d in doc.taxes %}
- {% if d.base_tax_amount %}
-
-
- {{ d.description }}
-
-
- {{ d.get_formatted("base_tax_amount") }}
-
-
- {% endif %}
- {% endfor %}
-
-
-
-
-
-
-
- {{ _("Grand Total") }}
- {{ doc.get_formatted("grand_total") }}
-
-
- {% endif %}
-
- {% if cart_settings.enable_checkout %}
-
- {{ _('Place Order') }}
-
- {% else %}
-
- {{ _('Request for Quote') }}
-
- {% endif %}
-
-
-
-
-
\ No newline at end of file
diff --git a/erpnext/templates/includes/integrations/gocardless_checkout.js b/erpnext/templates/includes/integrations/gocardless_checkout.js
deleted file mode 100644
index b18d55090c..0000000000
--- a/erpnext/templates/includes/integrations/gocardless_checkout.js
+++ /dev/null
@@ -1,24 +0,0 @@
-$(document).ready(function() {
- var data = {{ frappe.form_dict | json }};
- var doctype = "{{ reference_doctype }}"
- var docname = "{{ reference_docname }}"
-
- frappe.call({
- method: "erpnext.templates.pages.integrations.gocardless_checkout.check_mandate",
- freeze: true,
- headers: {
- "X-Requested-With": "XMLHttpRequest"
- },
- args: {
- "data": JSON.stringify(data),
- "reference_doctype": doctype,
- "reference_docname": docname
- },
- callback: function(r) {
- if (r.message) {
- window.location.href = r.message.redirect_to
- }
- }
- })
-
-})
diff --git a/erpnext/templates/includes/integrations/gocardless_confirmation.js b/erpnext/templates/includes/integrations/gocardless_confirmation.js
deleted file mode 100644
index fee1d2b632..0000000000
--- a/erpnext/templates/includes/integrations/gocardless_confirmation.js
+++ /dev/null
@@ -1,24 +0,0 @@
-$(document).ready(function() {
- var redirect_flow_id = "{{ redirect_flow_id }}";
- var doctype = "{{ reference_doctype }}";
- var docname = "{{ reference_docname }}";
-
- frappe.call({
- method: "erpnext.templates.pages.integrations.gocardless_confirmation.confirm_payment",
- freeze: true,
- headers: {
- "X-Requested-With": "XMLHttpRequest"
- },
- args: {
- "redirect_flow_id": redirect_flow_id,
- "reference_doctype": doctype,
- "reference_docname": docname
- },
- callback: function(r) {
- if (r.message) {
- window.location.href = r.message.redirect_to;
- }
- }
- });
-
-});
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
deleted file mode 100644
index d7adae562e..0000000000
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends 'frappe/templates/includes/navbar/navbar_items.html' %}
-
-{% block navbar_right_extension %}
-
-
-
-
-
-
-
-
- {% if frappe.db.get_single_value("E Commerce Settings", "enable_wishlist") %}
-
-
-
-
-
-
-
-
- {% endif %}
-{% endblock %}
diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html
deleted file mode 100644
index d95b28961c..0000000000
--- a/erpnext/templates/includes/order/order_macros.html
+++ /dev/null
@@ -1,52 +0,0 @@
-{% from "erpnext/templates/includes/macros.html" import product_image %}
-
-{% macro item_name_and_description(d) %}
-
-
-
- {% if d.thumbnail or d.image %}
- {{ product_image(d.thumbnail or d.image, no_border=True) }}
- {% else %}
-
- {{ frappe.utils.get_abbr(d.item_name) or "NA" }}
-
- {% endif %}
-
-
-
- {{ d.item_code }}
-
- {{ html2text(d.description) | truncate(140) }}
-
-
- {{ _("Qty ") }}({{ d.get_formatted("qty") }})
-
-
-
-{% endmacro %}
-
-{% macro item_name_and_description_cart(d) %}
-
-
-
- {{ product_image_square(d.thumbnail or d.image) }}
-
-
-
- {{ d.item_name|truncate(25) }}
-
-
-
- –
-
-
-
-
- +
-
-
-
-
-{% endmacro %}
diff --git a/erpnext/templates/includes/product_page.js b/erpnext/templates/includes/product_page.js
deleted file mode 100644
index a3979d037b..0000000000
--- a/erpnext/templates/includes/product_page.js
+++ /dev/null
@@ -1,217 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
-
-frappe.ready(function() {
- window.item_code = $('[itemscope] [itemprop="productID"]').text().trim();
- var qty = 0;
-
- frappe.call({
- type: "POST",
- method: "erpnext.e_commerce.shopping_cart.product_info.get_product_info_for_website",
- args: {
- item_code: get_item_code()
- },
- callback: function(r) {
- if(r.message) {
- if(r.message.cart_settings.enabled) {
- let hide_add_to_cart = !r.message.product_info.price
- || (!r.message.product_info.in_stock && !r.message.cart_settings.allow_items_not_in_stock);
- $(".item-cart, .item-price, .item-stock").toggleClass('hide', hide_add_to_cart);
- }
- if(r.message.cart_settings.show_price) {
- $(".item-price").toggleClass("hide", false);
- }
- if(r.message.cart_settings.show_stock_availability) {
- $(".item-stock").toggleClass("hide", false);
- }
- if(r.message.product_info.price) {
- $(".item-price")
- .html(r.message.product_info.price.formatted_price_sales_uom + "
\
- (" + r.message.product_info.price.formatted_price + " / " + r.message.product_info.uom + ")
");
-
- if(r.message.product_info.in_stock===0) {
- $(".item-stock").html("
{{ _("Not in stock") }}
");
- }
- else if(r.message.product_info.in_stock===1 && r.message.cart_settings.show_stock_availability) {
- var qty_display = "{{ _("In stock") }}";
- if (r.message.product_info.show_stock_qty) {
- qty_display += " ("+r.message.product_info.stock_qty+")";
- }
- $(".item-stock").html("
\
- "+qty_display+"
");
- }
-
- if(r.message.product_info.qty) {
- qty = r.message.product_info.qty;
- toggle_update_cart(r.message.product_info.qty);
- } else {
- toggle_update_cart(0);
- }
- }
- }
- }
- })
-
- $("#item-add-to-cart button").on("click", function() {
- frappe.provide('erpnext.shopping_cart');
-
- erpnext.shopping_cart.update_cart({
- item_code: get_item_code(),
- qty: $("#item-spinner .cart-qty").val(),
- callback: function(r) {
- if(!r.exc) {
- toggle_update_cart(1);
- qty = 1;
- }
- },
- btn: this,
- });
- });
-
- $("#item-spinner").on('click', '.number-spinner button', function () {
- var btn = $(this),
- input = btn.closest('.number-spinner').find('input'),
- oldValue = input.val().trim(),
- newVal = 0;
-
- if (btn.attr('data-dir') == 'up') {
- newVal = Number.parseInt(oldValue) + 1;
- } else if (btn.attr('data-dir') == 'dwn') {
- if (Number.parseInt(oldValue) > 1) {
- newVal = Number.parseInt(oldValue) - 1;
- }
- else {
- newVal = Number.parseInt(oldValue);
- }
- }
- input.val(newVal);
- });
-
- $("[itemscope] .item-view-attribute .form-control").on("change", function() {
- try {
- var item_code = encodeURIComponent(get_item_code());
-
- } catch(e) {
- // unable to find variant
- // then chose the closest available one
-
- var attribute = $(this).attr("data-attribute");
- var attribute_value = $(this).val();
- var item_code = find_closest_match(attribute, attribute_value);
-
- if (!item_code) {
- frappe.msgprint(__("Cannot find a matching Item. Please select some other value for {0}.", [attribute]))
- throw e;
- }
- }
-
- if (window.location.search == ("?variant=" + item_code) || window.location.search.includes(item_code)) {
- return;
- }
-
- window.location.href = window.location.pathname + "?variant=" + item_code;
- });
-
- // change the item image src when alternate images are hovered
- $(document.body).on('mouseover', '.item-alternative-image', (e) => {
- const $alternative_image = $(e.currentTarget);
- const src = $alternative_image.find('img').prop('src');
- $('.item-image img').prop('src', src);
- });
-});
-
-var toggle_update_cart = function(qty) {
- $("#item-add-to-cart").toggle(qty ? false : true);
- $("#item-update-cart")
- .toggle(qty ? true : false)
- .find("input").val(qty);
- $("#item-spinner").toggle(qty ? false : true);
-}
-
-function get_item_code() {
- var variant_info = window.variant_info;
- if(variant_info) {
- var attributes = get_selected_attributes();
- var no_of_attributes = Object.keys(attributes).length;
-
- for(var i in variant_info) {
- var variant = variant_info[i];
-
- if (variant.attributes.length < no_of_attributes) {
- // the case when variant has less attributes than template
- continue;
- }
-
- var match = true;
- for(var j in variant.attributes) {
- if(attributes[variant.attributes[j].attribute]
- != variant.attributes[j].attribute_value
- ) {
- match = false;
- break;
- }
- }
- if(match) {
- return variant.name;
- }
- }
- throw "Unable to match variant";
- } else {
- return window.item_code;
- }
-}
-
-function find_closest_match(selected_attribute, selected_attribute_value) {
- // find the closest match keeping the selected attribute in focus and get the item code
-
- var attributes = get_selected_attributes();
-
- var previous_match_score = 0;
- var previous_no_of_attributes = 0;
- var matched;
-
- var variant_info = window.variant_info;
- for(var i in variant_info) {
- var variant = variant_info[i];
- var match_score = 0;
- var has_selected_attribute = false;
-
- for(var j in variant.attributes) {
- if(attributes[variant.attributes[j].attribute]===variant.attributes[j].attribute_value) {
- match_score = match_score + 1;
-
- if (variant.attributes[j].attribute==selected_attribute && variant.attributes[j].attribute_value==selected_attribute_value) {
- has_selected_attribute = true;
- }
- }
- }
-
- if (has_selected_attribute
- && ((match_score > previous_match_score) || (match_score==previous_match_score && previous_no_of_attributes < variant.attributes.length))) {
- previous_match_score = match_score;
- matched = variant;
- previous_no_of_attributes = variant.attributes.length;
-
-
- }
- }
-
- if (matched) {
- for (var j in matched.attributes) {
- var attr = matched.attributes[j];
- $('[itemscope]')
- .find(repl('.item-view-attribute .form-control[data-attribute="%(attribute)s"]', attr))
- .val(attr.attribute_value);
- }
-
- return matched.name;
- }
-}
-
-function get_selected_attributes() {
- var attributes = {};
- $('[itemscope]').find(".item-view-attribute .form-control").each(function() {
- attributes[$(this).attr('data-attribute')] = $(this).val();
- });
- return attributes;
-}
diff --git a/erpnext/templates/includes/rfq/rfq_macros.html b/erpnext/templates/includes/rfq/rfq_macros.html
index 88724c30de..78ec6ff5f8 100644
--- a/erpnext/templates/includes/rfq/rfq_macros.html
+++ b/erpnext/templates/includes/rfq/rfq_macros.html
@@ -1,19 +1,25 @@
{% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %}
{% macro item_name_and_description(d, doc) %}
-
-
- {{ product_image(d.image) }}
-
-
- {{ d.item_code }}
-
{{ d.description }}
+
+
+ {% if d.image %}
+ {{ product_image(d.image) }}
+ {% else %}
+
+ {{ frappe.utils.get_abbr(d.item_name)}}
+
+ {% endif %}
+
+
+ {{ d.item_code }}
+
{{ d.description }}
{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}
{% if supplier_part_no %}
{{_("Supplier Part No") + ": "+ supplier_part_no}}
{% endif %}
-
-
+
+
{% endmacro %}
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
deleted file mode 100644
index 2b7d9e3523..0000000000
--- a/erpnext/templates/pages/cart.html
+++ /dev/null
@@ -1,132 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block title %} {{ _("Shopping Cart") }} {% endblock %}
-
-{% block header %}
- {% if show_pay_button %}
-
-
-
- {% endif %}
{% endblock %}
@@ -130,42 +118,6 @@
- {% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
- or (doc.doctype=="Sales Invoice" and doc.outstanding_amount> 0)) %}
-