+ `;
+
+ 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
new file mode 100644
index 0000000000..59c7f32fd4
--- /dev/null
+++ b/erpnext/e_commerce/redisearch_utils.py
@@ -0,0 +1,210 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.utils.redis_wrapper import RedisWrapper
+from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
+
+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_search_module_loaded():
+ try:
+ cache = frappe.cache()
+ out = cache.execute_command('MODULE LIST')
+
+ parsed_output = " ".join(
+ (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
+ )
+ return "search" in parsed_output
+ except Exception:
+ return False
+
+def if_redisearch_loaded(function):
+ "Decorator to check if Redisearch is loaded."
+ def wrapper(*args, **kwargs):
+ if is_search_module_loaded():
+ func = function(*args, **kwargs)
+ return func
+ return
+
+ return wrapper
+
+def make_key(key):
+ return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
+
+@if_redisearch_loaded
+def create_website_items_index():
+ "Creates Index Definition."
+
+ # CREATE index
+ client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
+
+ # DROP if already exists
+ try:
+ client.drop_index()
+ except Exception:
+ pass
+
+ idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
+
+ # Based on 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 = list(map(to_search_field, idx_fields))
+
+ client.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_loaded
+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 k, v in web_item.items():
+ super(RedisWrapper, cache).hset(make_key(key), k, v)
+
+ insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
+
+@if_redisearch_loaded
+def insert_to_name_ac(web_name, doc_name):
+ ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
+ ac.add_suggestions(Suggestion(web_name, payload=doc_name))
+
+def create_web_item_map(website_item_doc):
+ fields_to_index = get_fields_indexed()
+ web_item = {}
+
+ for f in fields_to_index:
+ web_item[f] = website_item_doc.get(f) or ''
+
+ return web_item
+
+@if_redisearch_loaded
+def update_index_for_item(website_item_doc):
+ # Reinsert to Cache
+ insert_item_to_index(website_item_doc)
+ define_autocomplete_dictionary()
+
+@if_redisearch_loaded
+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:
+ return False
+
+ delete_from_ac_dict(website_item_doc)
+ return True
+
+@if_redisearch_loaded
+def delete_from_ac_dict(website_item_doc):
+ '''Removes this items's name from autocomplete dictionary'''
+ cache = frappe.cache()
+ name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+ name_ac.delete(website_item_doc.web_item_name)
+
+@if_redisearch_loaded
+def define_autocomplete_dictionary():
+ """Creates an autocomplete search dictionary for `name`.
+ Also creats autocomplete dictionary for `categories` if
+ checked in E Commerce Settings"""
+
+ cache = frappe.cache()
+ name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+ cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
+
+ ac_categories = frappe.db.get_single_value(
+ 'E Commerce Settings',
+ 'show_categories_in_search_autocomplete'
+ )
+
+ # Delete both autocomplete dicts
+ try:
+ cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
+ cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
+ except Exception:
+ return False
+
+ items = frappe.get_all(
+ 'Website Item',
+ fields=['web_item_name', 'item_group'],
+ filters={"published": 1}
+ )
+
+ for item in items:
+ name_ac.add_suggestions(Suggestion(item.web_item_name))
+ if ac_categories and item.item_group:
+ cat_ac.add_suggestions(Suggestion(item.item_group))
+
+ return True
+
+@if_redisearch_loaded
+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 k, v in web_item.items():
+ super(RedisWrapper, cache).hset(key, k, v)
+
+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
+
+# TODO: Remove later
+# # Figure out a way to run this at startup
+define_autocomplete_dictionary()
+create_website_items_index()
diff --git a/erpnext/hotels/doctype/hotel_room_reservation_item/__init__.py b/erpnext/e_commerce/shopping_cart/__init__.py
similarity index 100%
rename from erpnext/hotels/doctype/hotel_room_reservation_item/__init__.py
rename to erpnext/e_commerce/shopping_cart/__init__.py
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
similarity index 88%
rename from erpnext/shopping_cart/cart.py
rename to erpnext/e_commerce/shopping_cart/cart.py
index ebbe233ca3..458cf69af7 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -1,7 +1,6 @@
# 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
@@ -11,20 +10,20 @@ 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.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
-from erpnext.utilities.product import get_qty_in_stock
+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("Shopping Cart Settings", "enabled")):
+ if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
if not quotation:
quotation = _get_cart_quotation()
- cart_count = cstr(len(quotation.get("items")))
+ cart_count = cstr(cint(quotation.get("total_qty")))
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
@@ -48,7 +47,7 @@ def get_cart_quotation(doc=None):
"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("Shopping Cart Settings")
+ "cart_settings": frappe.get_cached_doc("E Commerce Settings")
}
@frappe.whitelist()
@@ -72,7 +71,7 @@ def get_billing_addresses(party=None):
@frappe.whitelist()
def place_order():
quotation = _get_cart_quotation()
- cart_settings = frappe.db.get_value("Shopping Cart Settings", None,
+ cart_settings = frappe.db.get_value("E Commerce Settings", None,
["company", "allow_items_not_in_stock"], as_dict=1)
quotation.company = cart_settings.company
@@ -92,13 +91,19 @@ def place_order():
if not cint(cart_settings.allow_items_not_in_stock):
for item in sales_order.get("items"):
- item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
- item.item_code, ["website_warehouse", "is_stock_item"])
+ 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_qty_in_stock(item.item_code, "website_warehouse")
+ item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
- throw(_("{1} Not in Stock").format(item.item_code))
+ throw(_("{0} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty[0][0]:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
@@ -156,19 +161,19 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
set_cart_count(quotation)
- context = get_cart_quotation(quotation)
-
if cint(with_items):
+ context = get_cart_quotation(quotation)
return {
"items": frappe.render_template("templates/includes/cart/cart_items.html",
context),
- "taxes": frappe.render_template("templates/includes/order/order_taxes.html",
+ "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,
- 'shopping_cart_menu': get_shopping_cart_menu(context)
+ 'name': quotation.name
}
@frappe.whitelist()
@@ -265,13 +270,36 @@ def guess_territory():
territory = frappe.db.get_value("Territory", geoip_country)
return territory or \
- frappe.db.get_value("Shopping Cart Settings", None, "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", []):
- d.update(frappe.db.get_value("Item", d.item_code,
- ["thumbnail", "website_image", "description", "route"], as_dict=True))
+ 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)
+ )
return doc
@@ -288,7 +316,7 @@ def _get_cart_quotation(party=None):
if quotation:
qdoc = frappe.get_doc("Quotation", quotation[0].name)
else:
- company = frappe.db.get_value("Shopping Cart Settings", None, ["company"])
+ 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-",
@@ -343,7 +371,7 @@ def apply_cart_settings(party=None, quotation=None):
if not quotation:
quotation = _get_cart_quotation(party)
- cart_settings = frappe.get_doc("Shopping Cart Settings")
+ cart_settings = frappe.get_doc("E Commerce Settings")
set_price_list_and_rate(quotation, cart_settings)
@@ -420,7 +448,7 @@ def get_party(user=None):
party_doctype = contact.links[0].link_doctype
party = contact.links[0].link_name
- cart_settings = frappe.get_doc("Shopping Cart Settings")
+ cart_settings = frappe.get_doc("E Commerce Settings")
debtors_account = ''
@@ -557,10 +585,20 @@ def get_shipping_rules(quotation=None, cart_settings=None):
if quotation.shipping_address_name:
country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
if country:
- shipping_rules = frappe.db.sql_list("""select distinct sr.name
- from `tabShipping Rule Country` src, `tabShipping Rule` sr
- where src.country = %s and
- sr.disabled != 1 and sr.name = src.parent""", country)
+ 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
diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py
similarity index 52%
rename from erpnext/shopping_cart/product_info.py
rename to erpnext/e_commerce/shopping_cart/product_info.py
index 977f12fb9e..595fed01d2 100644
--- a/erpnext/shopping_cart/product_info.py
+++ b/erpnext/e_commerce/shopping_cart/product_info.py
@@ -1,15 +1,18 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
import frappe
-from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
show_quantity_in_website,
)
-from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock
+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)
@@ -18,7 +21,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
cart_settings = get_shopping_cart_settings()
if not cart_settings.enabled:
- return frappe._dict()
+ # 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:
@@ -26,25 +33,43 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None)
- price = get_price(
- item_code,
- selling_price_list,
- cart_settings.default_customer_group,
- cart_settings.company
- )
+ 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 = get_qty_in_stock(item_code, "website_warehouse")
+ 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,
- "stock_qty": stock_status.stock_qty,
- "in_stock": stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse"),
"qty": 0,
"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
- "show_stock_qty": show_quantity_in_website(),
"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
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
similarity index 77%
rename from erpnext/shopping_cart/test_shopping_cart.py
rename to erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 60c220a087..8519e68d09 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -8,8 +8,14 @@ import frappe
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
-from erpnext.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
-from erpnext.tests.utils import create_test_contact_and_address
+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,
+ update_cart,
+)
+from erpnext.tests.utils import change_settings, create_test_contact_and_address
# test_dependencies = ['Payment Terms Template']
@@ -27,8 +33,14 @@ class TestShoppingCart(unittest.TestCase):
frappe.set_user("Administrator")
create_test_contact_and_address()
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()
@@ -123,6 +135,43 @@ class TestShoppingCart(unittest.TestCase):
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)
+
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
@@ -166,7 +215,7 @@ class TestShoppingCart(unittest.TestCase):
# helper functions
def enable_shopping_cart(self):
- settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
+ settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.update({
"enabled": 1,
@@ -196,7 +245,7 @@ class TestShoppingCart(unittest.TestCase):
frappe.local.shopping_cart_settings = None
def disable_shopping_cart(self):
- settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
+ settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.enabled = 0
settings.save()
frappe.local.shopping_cart_settings = None
diff --git a/erpnext/shopping_cart/utils.py b/erpnext/e_commerce/shopping_cart/utils.py
similarity index 84%
rename from erpnext/shopping_cart/utils.py
rename to erpnext/e_commerce/shopping_cart/utils.py
index 5f0c792381..e9745a44d7 100644
--- a/erpnext/shopping_cart/utils.py
+++ b/erpnext/e_commerce/shopping_cart/utils.py
@@ -1,10 +1,8 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
- is_cart_enabled,
-)
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
def show_cart_count():
@@ -23,7 +21,7 @@ def set_cart_count(login_manager):
return
if show_cart_count():
- from erpnext.shopping_cart.cart import set_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)
diff --git a/erpnext/hotels/doctype/hotel_room_type/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py
similarity index 100%
rename from erpnext/hotels/doctype/hotel_room_type/__init__.py
rename to erpnext/e_commerce/variant_selector/__init__.py
diff --git a/erpnext/portal/product_configurator/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
similarity index 82%
rename from erpnext/portal/product_configurator/item_variants_cache.py
rename to erpnext/e_commerce/variant_selector/item_variants_cache.py
index 636ae8d491..bb6b3ef37f 100644
--- a/erpnext/portal/product_configurator/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
val = frappe.cache().get_value('ordered_attribute_values_map')
if val: return val
- all_attribute_values = frappe.db.get_all('Item Attribute Value',
+ all_attribute_values = frappe.get_all('Item Attribute Value',
['attribute_value', 'idx', 'parent'], order_by='idx asc')
ordered_attribute_values_map = frappe._dict({})
@@ -57,22 +57,35 @@ class ItemVariantsCacheManager:
def build_cache(self):
parent_item_code = self.item_code
- attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
- {'parent': parent_item_code}, ['attribute'], order_by='idx asc')
+ attributes = [
+ a.attribute for a in frappe.get_all(
+ 'Item Variant Attribute',
+ {'parent': parent_item_code},
+ ['attribute'],
+ order_by='idx asc'
+ )
]
- item_variants_data = frappe.db.get_all('Item Variant Attribute',
- {'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
+ # join with Website Item
+ item_variants_data = frappe.get_all(
+ 'Item Variant Attribute',
+ {'variant_of': parent_item_code},
+ ['parent', 'attribute', 'attribute_value'],
order_by='name',
as_list=1
)
- disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
+ disabled_items = set(
+ [i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
+ )
- attribute_value_item_map = frappe._dict({})
- item_attribute_value_map = frappe._dict({})
+ attribute_value_item_map = frappe._dict()
+ item_attribute_value_map = frappe._dict()
+ # dont consider variants that are disabled
+ # pull all other variants
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
+
for row in item_variants_data:
item_code, attribute, attribute_value = row
# (attr, value) => [item1, item2]
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
new file mode 100644
index 0000000000..b83961e6e1
--- /dev/null
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -0,0 +1,117 @@
+import frappe
+
+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
+from erpnext.tests.utils import ERPNextTestCase
+
+test_dependencies = ["Item"]
+
+class TestVariantSelector(ERPNextTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ 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)
+ 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")
\ No newline at end of file
diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py
new file mode 100644
index 0000000000..33802737ef
--- /dev/null
+++ b/erpnext/e_commerce/variant_selector/utils.py
@@ -0,0 +1,218 @@
+import frappe
+from frappe.utils import cint
+
+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):
+ '''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["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
+ else:
+ product_info = None
+
+ 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
+ }
+
+
+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/hotels/doctype/hotel_settings/__init__.py b/erpnext/e_commerce/web_template/__init__.py
similarity index 100%
rename from erpnext/hotels/doctype/hotel_settings/__init__.py
rename to erpnext/e_commerce/web_template/__init__.py
diff --git a/erpnext/hotels/report/__init__.py b/erpnext/e_commerce/web_template/hero_slider/__init__.py
similarity index 100%
rename from erpnext/hotels/report/__init__.py
rename to erpnext/e_commerce/web_template/hero_slider/__init__.py
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
similarity index 100%
rename from erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
rename to erpnext/e_commerce/web_template/hero_slider/hero_slider.html
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json
similarity index 98%
rename from erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
rename to erpnext/e_commerce/web_template/hero_slider/hero_slider.json
index 04fb1d2705..2b1807c965 100644
--- a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
+++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json
@@ -1,4 +1,5 @@
{
+ "__unsaved": 1,
"creation": "2020-11-17 15:21:51.207221",
"docstatus": 0,
"doctype": "Web Template",
@@ -273,9 +274,9 @@
}
],
"idx": 2,
- "modified": "2020-12-29 12:30:02.794994",
+ "modified": "2021-02-24 15:57:05.889709",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Hero Slider",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/hotels/report/hotel_room_occupancy/__init__.py b/erpnext/e_commerce/web_template/item_card_group/__init__.py
similarity index 100%
rename from erpnext/hotels/report/hotel_room_occupancy/__init__.py
rename to erpnext/e_commerce/web_template/item_card_group/__init__.py
diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html
similarity index 81%
rename from erpnext/shopping_cart/web_template/item_card_group/item_card_group.html
rename to erpnext/e_commerce/web_template/item_card_group/item_card_group.html
index fe061d5f5f..07952f056a 100644
--- a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html
+++ b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html
@@ -23,11 +23,10 @@
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
{%- set item = values['card_' + index + '_item'] -%}
{%- if item -%}
- {%- set item = frappe.get_doc("Item", item) -%}
+ {%- set web_item = frappe.get_doc("Website Item", item) -%}
{{ item_card(
- item.item_name, item.image, item.route, item.description,
- None, item.item_group, values['card_' + index + '_featured'],
- True, "Center"
+ web_item, is_featured=values['card_' + index + '_featured'],
+ is_full_width=True, align="Center"
) }}
{%- endif -%}
{%- endfor -%}
diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json b/erpnext/e_commerce/web_template/item_card_group/item_card_group.json
similarity index 84%
rename from erpnext/shopping_cart/web_template/item_card_group/item_card_group.json
rename to erpnext/e_commerce/web_template/item_card_group/item_card_group.json
index ad087b0470..ad9e2a7b24 100644
--- a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json
+++ b/erpnext/e_commerce/web_template/item_card_group/item_card_group.json
@@ -17,15 +17,12 @@
"reqd": 0
},
{
- "__unsaved": 1,
"fieldname": "primary_action_label",
"fieldtype": "Data",
"label": "Primary Action Label",
"reqd": 0
},
{
- "__islocal": 1,
- "__unsaved": 1,
"fieldname": "primary_action",
"fieldtype": "Data",
"label": "Primary Action",
@@ -40,8 +37,8 @@
{
"fieldname": "card_1_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -59,8 +56,8 @@
{
"fieldname": "card_2_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -79,8 +76,8 @@
{
"fieldname": "card_3_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -98,8 +95,8 @@
{
"fieldname": "card_4_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -117,8 +114,8 @@
{
"fieldname": "card_5_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -136,8 +133,8 @@
{
"fieldname": "card_6_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -155,8 +152,8 @@
{
"fieldname": "card_7_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -174,8 +171,8 @@
{
"fieldname": "card_8_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -193,8 +190,8 @@
{
"fieldname": "card_9_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -212,8 +209,8 @@
{
"fieldname": "card_10_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -231,8 +228,8 @@
{
"fieldname": "card_11_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -250,8 +247,8 @@
{
"fieldname": "card_12_item",
"fieldtype": "Link",
- "label": "Item",
- "options": "Item",
+ "label": "Website Item",
+ "options": "Website Item",
"reqd": 0
},
{
@@ -262,9 +259,9 @@
}
],
"idx": 0,
- "modified": "2020-11-19 18:48:52.633045",
+ "modified": "2021-12-21 14:44:59.821335",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Item Card Group",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/patches/v14_0/__init__.py b/erpnext/e_commerce/web_template/product_card/__init__.py
similarity index 100%
rename from erpnext/patches/v14_0/__init__.py
rename to erpnext/e_commerce/web_template/product_card/__init__.py
diff --git a/erpnext/shopping_cart/web_template/product_card/product_card.html b/erpnext/e_commerce/web_template/product_card/product_card.html
similarity index 100%
rename from erpnext/shopping_cart/web_template/product_card/product_card.html
rename to erpnext/e_commerce/web_template/product_card/product_card.html
diff --git a/erpnext/shopping_cart/web_template/product_card/product_card.json b/erpnext/e_commerce/web_template/product_card/product_card.json
similarity index 82%
rename from erpnext/shopping_cart/web_template/product_card/product_card.json
rename to erpnext/e_commerce/web_template/product_card/product_card.json
index 1059c1b251..2eb73741ef 100644
--- a/erpnext/shopping_cart/web_template/product_card/product_card.json
+++ b/erpnext/e_commerce/web_template/product_card/product_card.json
@@ -5,7 +5,6 @@
"doctype": "Web Template",
"fields": [
{
- "__unsaved": 1,
"fieldname": "item",
"fieldtype": "Link",
"label": "Item",
@@ -13,7 +12,6 @@
"reqd": 0
},
{
- "__unsaved": 1,
"fieldname": "featured",
"fieldtype": "Check",
"label": "Featured",
@@ -22,9 +20,9 @@
}
],
"idx": 0,
- "modified": "2020-11-17 15:33:34.982515",
+ "modified": "2021-02-24 16:05:17.926610",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Product Card",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/web_template/product_category_cards/__init__.py
similarity index 100%
rename from erpnext/portal/doctype/products_settings/__init__.py
rename to erpnext/e_commerce/web_template/product_category_cards/__init__.py
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
similarity index 81%
rename from erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html
rename to erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
index 06b76af901..6d75a8b1d5 100644
--- a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html
+++ b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
@@ -6,8 +6,15 @@
}) -%}
{% if image %}
-
+
+ {% else %}
+
+
+ {{ frappe.utils.get_abbr(title or '') }}
+
+
{% endif %}
+
{{ title or '' }}
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
similarity index 95%
rename from erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json
rename to erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
index ba5f63b48b..0202165d08 100644
--- a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json
+++ b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
@@ -74,9 +74,9 @@
}
],
"idx": 0,
- "modified": "2020-11-18 17:26:28.726260",
+ "modified": "2021-02-24 16:03:33.835635",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Product Category Cards",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index a23d49267e..4d0f3a9801 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
+from frappe.query_builder.functions import Min
from frappe.utils import comma_and, get_link_to_form, getdate
@@ -60,8 +61,15 @@ class ProgramEnrollment(Document):
frappe.throw(_("Student is already enrolled."))
def update_student_joining_date(self):
- date = frappe.db.sql("select min(enrollment_date) from `tabProgram Enrollment` where student= %s", self.student)
- frappe.db.set_value("Student", self.student, "joining_date", date)
+ table = frappe.qb.DocType('Program Enrollment')
+ date = (
+ frappe.qb.from_(table)
+ .select(Min(table.enrollment_date).as_('enrollment_date'))
+ .where(table.student == self.student)
+ ).run(as_dict=True)
+
+ if date:
+ frappe.db.set_value("Student", self.student, "joining_date", date[0].enrollment_date)
def make_fee_records(self):
from erpnext.education.api import get_fee_components
diff --git a/erpnext/education/workspace/education/education.json b/erpnext/education/workspace/education/education.json
index 1465295658..0c7f19894c 100644
--- a/erpnext/education/workspace/education/education.json
+++ b/erpnext/education/workspace/education/education.json
@@ -5,7 +5,7 @@
"label": "Program Enrollments"
}
],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Education\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Program Enrollments\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Instructor\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Program\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fees\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course Scheduling Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Attendance Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Student and Instructor\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Content Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Admission\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fees\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Schedule\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"LMS Activity\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Education\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Program Enrollments\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Student\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Instructor\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Program\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fees\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Student Monthly Attendance Sheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Scheduling Tool\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Student Attendance Tool\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Student and Instructor\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Content Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Admission\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fees\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Schedule\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"LMS Activity\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assessment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assessment Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 17:22:57.066401",
"docstatus": 0,
"doctype": "Workspace",
@@ -692,7 +692,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:57.929276",
+ "modified": "2022-01-13 17:29:13.676542",
"modified_by": "Administrator",
"module": "Education",
"name": "Education",
@@ -701,7 +701,7 @@
"public": 1,
"restrict_to_domain": "Education",
"roles": [],
- "sequence_id": 9,
+ "sequence_id": 9.0,
"shortcuts": [
{
"color": "Grey",
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
index 66826ba8d7..29bc36f384 100644
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
@@ -5,11 +5,11 @@
import csv
import math
import time
+from io import StringIO
import dateutil
import frappe
from frappe import _
-from six import StringIO
import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
@@ -149,7 +149,6 @@ def create_item_code(amazon_item_json, sku):
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
item.brand = new_brand
item.manufacturer = new_manufacturer
- item.web_long_description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index e242ace60f..a8119ac86c 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -2,13 +2,14 @@
# 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_payment_gateway, create_request_log
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, flt, get_url
-from six.moves.urllib.parse import urlencode
class GoCardlessSettings(Document):
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
index 8da52f49f1..309d2cb0e3 100644
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
+++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
@@ -2,12 +2,13 @@
# For license information, please see license.txt
+from urllib.parse import urlparse
+
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.document import Document
from frappe.utils.nestedset import get_root_of
-from six.moves.urllib.parse import urlparse
class WoocommerceSettings(Document):
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index d922d875fd..30d3948fee 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -1,10 +1,10 @@
import base64
import hashlib
import hmac
+from urllib.parse import urlparse
import frappe
from frappe import _
-from six.moves.urllib.parse import urlparse
from erpnext import get_default_company
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 8e4f92791a..45077aa66c 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Marketplace\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Marketplace\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-08-20 19:30:48.138801",
"docstatus": 0,
"doctype": "Workspace",
@@ -40,17 +40,6 @@
"onboard": 0,
"type": "Link"
},
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Shopify Settings",
- "link_count": 0,
- "link_to": "Shopify Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -112,7 +101,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:58.740247",
+ "modified": "2022-01-13 17:35:35.508718",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
@@ -121,7 +110,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 10,
+ "sequence_id": 10.0,
"shortcuts": [],
"title": "ERPNext Integrations"
}
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index f014b0e1e9..0e290384b4 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -51,15 +51,15 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin
on_session_creation = [
"erpnext.portal.utils.create_customer_or_supplier",
- "erpnext.shopping_cart.utils.set_cart_count"
+ "erpnext.e_commerce.shopping_cart.utils.set_cart_count"
]
-on_logout = "erpnext.shopping_cart.utils.clear_cart_count"
+on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count"
treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department']
# website
-update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
-my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
+update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.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", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
@@ -67,14 +67,13 @@ calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday
domains = {
'Distribution': 'erpnext.domains.distribution',
'Education': 'erpnext.domains.education',
- 'Hospitality': 'erpnext.domains.hospitality',
'Manufacturing': 'erpnext.domains.manufacturing',
'Non Profit': 'erpnext.domains.non_profit',
'Retail': 'erpnext.domains.retail',
'Services': 'erpnext.domains.services',
}
-website_generators = ["Item Group", "Item", "BOM", "Sales Partner",
+website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner",
"Job Opening", "Student Admission"]
website_context = {
@@ -238,10 +237,7 @@ doc_events = {
]
},
"Sales Taxes and Charges Template": {
- "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
- },
- "Website Settings": {
- "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
+ "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
},
"Tax Category": {
"validate": "erpnext.regional.india.utils.validate_tax_category"
@@ -444,6 +440,7 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
+ 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
},
@@ -579,13 +576,6 @@ global_search_doctypes = {
{'doctype': 'Donor Type', 'index': 10},
{'doctype': 'Membership Type', 'index': 11}
],
- "Hospitality": [
- {'doctype': 'Hotel Room', 'index': 0},
- {'doctype': 'Hotel Room Reservation', 'index': 1},
- {'doctype': 'Hotel Room Pricing', 'index': 2},
- {'doctype': 'Hotel Room Package', 'index': 3},
- {'doctype': 'Hotel Room Type', 'index': 4}
- ]
}
additional_timeline_content = {
diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.js b/erpnext/hotels/doctype/hotel_room/hotel_room.js
deleted file mode 100644
index 76f22d5d4e..0000000000
--- a/erpnext/hotels/doctype/hotel_room/hotel_room.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Room', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.json b/erpnext/hotels/doctype/hotel_room/hotel_room.json
deleted file mode 100644
index 2567c077b6..0000000000
--- a/erpnext/hotels/doctype/hotel_room/hotel_room.json
+++ /dev/null
@@ -1,175 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "prompt",
- "beta": 1,
- "creation": "2017-12-08 12:33:56.320420",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "hotel_room_type",
- "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": "Hotel Room Type",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Type",
- "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": "capacity",
- "fieldtype": "Int",
- "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": "Capacity",
- "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": "extra_bed_capacity",
- "fieldtype": "Int",
- "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": "Extra Bed Capacity",
- "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": "2017-12-09 12:10:50.670113",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room",
- "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
- },
- {
- "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": "Hotel Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.py b/erpnext/hotels/doctype/hotel_room/hotel_room.py
deleted file mode 100644
index e4bd1c8846..0000000000
--- a/erpnext/hotels/doctype/hotel_room/hotel_room.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class HotelRoom(Document):
- def validate(self):
- if not self.capacity:
- self.capacity, self.extra_bed_capacity = frappe.db.get_value('Hotel Room Type',
- self.hotel_room_type, ['capacity', 'extra_bed_capacity'])
diff --git a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py
deleted file mode 100644
index 95efe2c606..0000000000
--- a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-test_dependencies = ["Hotel Room Package"]
-test_records = [
- dict(doctype="Hotel Room", name="1001",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1002",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1003",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1004",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1005",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1006",
- hotel_room_type="Basic Room")
-]
-
-class TestHotelRoom(unittest.TestCase):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.json b/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.json
deleted file mode 100644
index 29a0407a8a..0000000000
--- a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-08 12:35:36.572185",
- "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": "item",
- "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": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "billable",
- "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": "Billable",
- "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": 1,
- "max_attachments": 0,
- "modified": "2017-12-09 12:05:07.125687",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Amenity",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.py b/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.py
deleted file mode 100644
index 166493124a..0000000000
--- a/erpnext/hotels/doctype/hotel_room_amenity/hotel_room_amenity.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelRoomAmenity(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.js b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.js
deleted file mode 100644
index 5b09ae568e..0000000000
--- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.js
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Room Package', {
- hotel_room_type: function(frm) {
- if (frm.doc.hotel_room_type) {
- frappe.model.with_doc('Hotel Room Type', frm.doc.hotel_room_type, () => {
- let hotel_room_type = frappe.get_doc('Hotel Room Type', frm.doc.hotel_room_type);
-
- // reset the amenities
- frm.doc.amenities = [];
-
- for (let amenity of hotel_room_type.amenities) {
- let d = frm.add_child('amenities');
- d.item = amenity.item;
- d.billable = amenity.billable;
- }
-
- frm.refresh();
- });
- }
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.json b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.json
deleted file mode 100644
index 57dad44b7d..0000000000
--- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.json
+++ /dev/null
@@ -1,215 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "prompt",
- "beta": 1,
- "creation": "2017-12-08 12:43:17.211064",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "hotel_room_type",
- "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": "Hotel Room Type",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Type",
- "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": "column_break_2",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item",
- "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": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "section_break_4",
- "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": "amenities",
- "fieldtype": "Table",
- "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": "Amenities",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Amenity",
- "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
- }
- ],
- "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": "2017-12-09 12:10:31.111952",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Package",
- "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": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py
deleted file mode 100644
index aedc83a846..0000000000
--- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class HotelRoomPackage(Document):
- def validate(self):
- if not self.item:
- item = frappe.get_doc(dict(
- doctype = 'Item',
- item_code = self.name,
- item_group = 'Products',
- is_stock_item = 0,
- stock_uom = 'Unit'
- ))
- item.insert()
- self.item = item.name
diff --git a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py
deleted file mode 100644
index 749731f491..0000000000
--- a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-test_records = [
- dict(doctype='Item', item_code='Breakfast',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Lunch',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Dinner',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='WiFi',
- item_group='Products', is_stock_item=0),
- dict(doctype='Hotel Room Type', name="Delux Room",
- capacity=4,
- extra_bed_capacity=2,
- amenities = [
- dict(item='WiFi', billable=0)
- ]),
- dict(doctype='Hotel Room Type', name="Basic Room",
- capacity=4,
- extra_bed_capacity=2,
- amenities = [
- dict(item='Breakfast', billable=0)
- ]),
- dict(doctype="Hotel Room Package", name="Basic Room with Breakfast",
- hotel_room_type="Basic Room",
- amenities = [
- dict(item="Breakfast", billable=0)
- ]),
- dict(doctype="Hotel Room Package", name="Basic Room with Lunch",
- hotel_room_type="Basic Room",
- amenities = [
- dict(item="Breakfast", billable=0),
- dict(item="Lunch", billable=0)
- ]),
- dict(doctype="Hotel Room Package", name="Basic Room with Dinner",
- hotel_room_type="Basic Room",
- amenities = [
- dict(item="Breakfast", billable=0),
- dict(item="Dinner", billable=0)
- ])
-]
-
-class TestHotelRoomPackage(unittest.TestCase):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.js b/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.js
deleted file mode 100644
index 87bb192570..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Room Pricing', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.json b/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.json
deleted file mode 100644
index 0f5a776211..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.json
+++ /dev/null
@@ -1,266 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "prompt",
- "beta": 1,
- "creation": "2017-12-08 12:51:47.088174",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "fieldname": "enabled",
- "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": "Enabled",
- "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": "currency",
- "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": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "Currency",
- "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": "from_date",
- "fieldtype": "Date",
- "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": "From Date",
- "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": "to_date",
- "fieldtype": "Date",
- "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": "To Date",
- "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_5",
- "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": "items",
- "fieldtype": "Table",
- "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": "Items",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Pricing Item",
- "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
- }
- ],
- "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": "2017-12-09 12:10:41.559559",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Pricing",
- "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
- },
- {
- "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": "Hotel Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.py
deleted file mode 100644
index d28e573426..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing/hotel_room_pricing.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelRoomPricing(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py
deleted file mode 100644
index 34550096dd..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-test_dependencies = ["Hotel Room Package"]
-test_records = [
- dict(doctype="Hotel Room Pricing", enabled=1,
- name="Winter 2017",
- from_date="2017-01-01", to_date="2017-01-10",
- items = [
- dict(item="Basic Room with Breakfast", rate=10000),
- dict(item="Basic Room with Lunch", rate=11000),
- dict(item="Basic Room with Dinner", rate=12000)
- ])
-]
-
-class TestHotelRoomPricing(unittest.TestCase):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.json b/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.json
deleted file mode 100644
index d6cd826bcc..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-08 12:50:13.486090",
- "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": "item",
- "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": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "rate",
- "fieldtype": "Currency",
- "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": "Rate",
- "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
- }
- ],
- "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": "2017-12-09 12:04:58.641703",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Pricing Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.py b/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.py
deleted file mode 100644
index 2e6bb5fac2..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing_item/hotel_room_pricing_item.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelRoomPricingItem(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.js b/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.js
deleted file mode 100644
index f6decd9e95..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Room Pricing Package', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.json b/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.json
deleted file mode 100644
index 1e529325cd..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.json
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-12-08 12:50:13.486090",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "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": "From Date",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "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": "To Date",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "hotel_room_package",
- "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": "Hotel Room Package",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Package",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "rate",
- "fieldtype": "Currency",
- "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": "Rate",
- "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,
- "translatable": 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": 1,
- "max_attachments": 0,
- "modified": "2018-11-04 03:34:02.551811",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Pricing Package",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.py b/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.py
deleted file mode 100644
index ebbdb6ec6c..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing_package/hotel_room_pricing_package.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelRoomPricingPackage(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.py b/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.py
deleted file mode 100644
index 196e6504b5..0000000000
--- a/erpnext/hotels/doctype/hotel_room_pricing_package/test_hotel_room_pricing_package.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestHotelRoomPricingPackage(unittest.TestCase):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.js b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.js
deleted file mode 100644
index e58d763e47..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.js
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Room Reservation', {
- refresh: function(frm) {
- if(frm.doc.docstatus == 1){
- frm.add_custom_button(__('Create Invoice'), ()=> {
- frm.trigger("make_invoice");
- });
- }
- },
- from_date: function(frm) {
- frm.trigger("recalculate_rates");
- },
- to_date: function(frm) {
- frm.trigger("recalculate_rates");
- },
- recalculate_rates: function(frm) {
- if (!frm.doc.from_date || !frm.doc.to_date
- || !frm.doc.items.length){
- return;
- }
- frappe.call({
- "method": "erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation.get_room_rate",
- "args": {"hotel_room_reservation": frm.doc}
- }).done((r)=> {
- for (var i = 0; i < r.message.items.length; i++) {
- frm.doc.items[i].rate = r.message.items[i].rate;
- frm.doc.items[i].amount = r.message.items[i].amount;
- }
- frappe.run_serially([
- ()=> frm.set_value("net_total", r.message.net_total),
- ()=> frm.refresh_field("items")
- ]);
- });
- },
- make_invoice: function(frm) {
- frappe.model.with_doc("Hotel Settings", "Hotel Settings", ()=>{
- frappe.model.with_doctype("Sales Invoice", ()=>{
- let hotel_settings = frappe.get_doc("Hotel Settings", "Hotel Settings");
- let invoice = frappe.model.get_new_doc("Sales Invoice");
- invoice.customer = frm.doc.customer || hotel_settings.default_customer;
- if (hotel_settings.default_invoice_naming_series){
- invoice.naming_series = hotel_settings.default_invoice_naming_series;
- }
- for (let d of frm.doc.items){
- let invoice_item = frappe.model.add_child(invoice, "items")
- invoice_item.item_code = d.item;
- invoice_item.qty = d.qty;
- invoice_item.rate = d.rate;
- }
- if (hotel_settings.default_taxes_and_charges){
- invoice.taxes_and_charges = hotel_settings.default_taxes_and_charges;
- }
- frappe.set_route("Form", invoice.doctype, invoice.name);
- });
- });
- }
-});
-
-frappe.ui.form.on('Hotel Room Reservation Item', {
- item: function(frm, doctype, name) {
- frm.trigger("recalculate_rates");
- },
- qty: function(frm) {
- frm.trigger("recalculate_rates");
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.json b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.json
deleted file mode 100644
index fd20efdf8d..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.json
+++ /dev/null
@@ -1,436 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "HTL-RES-.YYYY.-.#####",
- "beta": 1,
- "creation": "2017-12-08 13:01:34.829175",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "guest_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": "Guest 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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 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": 0,
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "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": "From Date",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "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": "To Date",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "late_checkin",
- "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": "Late Checkin",
- "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
- },
- {
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "status",
- "fieldtype": "Select",
- "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": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "Booked\nAdvance Paid\nInvoiced\nPaid\nCompleted\nCancelled",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_8",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "items",
- "fieldtype": "Table",
- "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": "Items",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Reservation Item",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "net_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": "Net Total",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "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": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Hotel Room Reservation",
- "permlevel": 0,
- "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
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:47.326951",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Reservation",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 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
- },
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Hotel Reservation User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py
deleted file mode 100644
index 7725955396..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import json
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import add_days, date_diff, flt
-
-
-class HotelRoomUnavailableError(frappe.ValidationError): pass
-class HotelRoomPricingNotSetError(frappe.ValidationError): pass
-
-class HotelRoomReservation(Document):
- def validate(self):
- self.total_rooms = {}
- self.set_rates()
- self.validate_availability()
-
- def validate_availability(self):
- for i in range(date_diff(self.to_date, self.from_date)):
- day = add_days(self.from_date, i)
- self.rooms_booked = {}
-
- for d in self.items:
- if not d.item in self.rooms_booked:
- self.rooms_booked[d.item] = 0
-
- room_type = frappe.db.get_value("Hotel Room Package",
- d.item, 'hotel_room_type')
- rooms_booked = get_rooms_booked(room_type, day, exclude_reservation=self.name) \
- + d.qty + self.rooms_booked.get(d.item)
- total_rooms = self.get_total_rooms(d.item)
- if total_rooms < rooms_booked:
- frappe.throw(_("Hotel Rooms of type {0} are unavailable on {1}").format(d.item,
- frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomUnavailableError)
-
- self.rooms_booked[d.item] += rooms_booked
-
- def get_total_rooms(self, item):
- if not item in self.total_rooms:
- self.total_rooms[item] = frappe.db.sql("""
- select count(*)
- from
- `tabHotel Room Package` package
- inner join
- `tabHotel Room` room on package.hotel_room_type = room.hotel_room_type
- where
- package.item = %s""", item)[0][0] or 0
-
- return self.total_rooms[item]
-
- def set_rates(self):
- self.net_total = 0
- for d in self.items:
- net_rate = 0.0
- for i in range(date_diff(self.to_date, self.from_date)):
- day = add_days(self.from_date, i)
- if not d.item:
- continue
- day_rate = frappe.db.sql("""
- select
- item.rate
- from
- `tabHotel Room Pricing Item` item,
- `tabHotel Room Pricing` pricing
- where
- item.parent = pricing.name
- and item.item = %s
- and %s between pricing.from_date
- and pricing.to_date""", (d.item, day))
-
- if day_rate:
- net_rate += day_rate[0][0]
- else:
- frappe.throw(
- _("Please set Hotel Room Rate on {}").format(
- frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomPricingNotSetError)
- d.rate = net_rate
- d.amount = net_rate * flt(d.qty)
- self.net_total += d.amount
-
-@frappe.whitelist()
-def get_room_rate(hotel_room_reservation):
- """Calculate rate for each day as it may belong to different Hotel Room Pricing Item"""
- doc = frappe.get_doc(json.loads(hotel_room_reservation))
- doc.set_rates()
- return doc.as_dict()
-
-def get_rooms_booked(room_type, day, exclude_reservation=None):
- exclude_condition = ''
- if exclude_reservation:
- exclude_condition = 'and reservation.name != {0}'.format(frappe.db.escape(exclude_reservation))
-
- return frappe.db.sql("""
- select sum(item.qty)
- from
- `tabHotel Room Package` room_package,
- `tabHotel Room Reservation Item` item,
- `tabHotel Room Reservation` reservation
- where
- item.parent = reservation.name
- and room_package.item = item.item
- and room_package.hotel_room_type = %s
- and reservation.docstatus = 1
- {exclude_condition}
- and %s between reservation.from_date
- and reservation.to_date""".format(exclude_condition=exclude_condition),
- (room_type, day))[0][0] or 0
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js
deleted file mode 100644
index 7bde292a2b..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation_calendar.js
+++ /dev/null
@@ -1,9 +0,0 @@
-frappe.views.calendar["Hotel Room Reservation"] = {
- field_map: {
- "start": "from_date",
- "end": "to_date",
- "id": "name",
- "title": "guest_name",
- "status": "status"
- }
-}
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py
deleted file mode 100644
index bb32a27fa7..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import (
- HotelRoomPricingNotSetError,
- HotelRoomUnavailableError,
-)
-
-test_dependencies = ["Hotel Room Package", "Hotel Room Pricing", "Hotel Room"]
-
-class TestHotelRoomReservation(unittest.TestCase):
- def setUp(self):
- frappe.db.sql("delete from `tabHotel Room Reservation`")
- frappe.db.sql("delete from `tabHotel Room Reservation Item`")
-
- def test_reservation(self):
- reservation = make_reservation(
- from_date="2017-01-01",
- to_date="2017-01-03",
- items=[
- dict(item="Basic Room with Dinner", qty=2)
- ]
- )
- reservation.insert()
- self.assertEqual(reservation.net_total, 48000)
-
- def test_price_not_set(self):
- reservation = make_reservation(
- from_date="2016-01-01",
- to_date="2016-01-03",
- items=[
- dict(item="Basic Room with Dinner", qty=2)
- ]
- )
- self.assertRaises(HotelRoomPricingNotSetError, reservation.insert)
-
- def test_room_unavailable(self):
- reservation = make_reservation(
- from_date="2017-01-01",
- to_date="2017-01-03",
- items=[
- dict(item="Basic Room with Dinner", qty=2),
- ]
- )
- reservation.insert()
-
- reservation = make_reservation(
- from_date="2017-01-01",
- to_date="2017-01-03",
- items=[
- dict(item="Basic Room with Dinner", qty=20),
- ]
- )
- self.assertRaises(HotelRoomUnavailableError, reservation.insert)
-
-def make_reservation(**kwargs):
- kwargs["doctype"] = "Hotel Room Reservation"
- if not "guest_name" in kwargs:
- kwargs["guest_name"] = "Test Guest"
- doc = frappe.get_doc(kwargs)
- return doc
diff --git a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.json b/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.json
deleted file mode 100644
index 2b7931ebc0..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.json
+++ /dev/null
@@ -1,195 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-12-08 12:58:21.733330",
- "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": "item",
- "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": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "qty",
- "fieldtype": "Int",
- "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": "Qty",
- "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": "currency",
- "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": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "Currency",
- "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": "rate",
- "fieldtype": "Currency",
- "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": "Rate",
- "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": "amount",
- "fieldtype": "Currency",
- "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": "Amount",
- "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": 1,
- "max_attachments": 0,
- "modified": "2017-12-09 12:04:34.562956",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Reservation Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.py b/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.py
deleted file mode 100644
index 41d86ddca6..0000000000
--- a/erpnext/hotels/doctype/hotel_room_reservation_item/hotel_room_reservation_item.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelRoomReservationItem(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.js b/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.js
deleted file mode 100644
index d73835db94..0000000000
--- a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Room Type', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.json b/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.json
deleted file mode 100644
index 3d26413cf8..0000000000
--- a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.json
+++ /dev/null
@@ -1,204 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "prompt",
- "beta": 1,
- "creation": "2017-12-08 12:38:29.485175",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "capacity",
- "fieldtype": "Int",
- "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": "Capacity",
- "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": "extra_bed_capacity",
- "fieldtype": "Int",
- "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": "Extra Bed Capacity",
- "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": "section_break_3",
- "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": "amenities",
- "fieldtype": "Table",
- "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": "Amenities",
- "length": 0,
- "no_copy": 0,
- "options": "Hotel Room Amenity",
- "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
- }
- ],
- "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": "2017-12-09 12:10:23.355486",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Type",
- "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
- },
- {
- "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": "Hotel Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.py b/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.py
deleted file mode 100644
index 7ab529fee9..0000000000
--- a/erpnext/hotels/doctype/hotel_room_type/hotel_room_type.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelRoomType(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.py b/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.py
deleted file mode 100644
index 8d1147d0f2..0000000000
--- a/erpnext/hotels/doctype/hotel_room_type/test_hotel_room_type.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestHotelRoomType(unittest.TestCase):
- pass
diff --git a/erpnext/hotels/doctype/hotel_settings/hotel_settings.js b/erpnext/hotels/doctype/hotel_settings/hotel_settings.js
deleted file mode 100644
index 0b4a2c36ca..0000000000
--- a/erpnext/hotels/doctype/hotel_settings/hotel_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Hotel Settings', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/hotels/doctype/hotel_settings/hotel_settings.json b/erpnext/hotels/doctype/hotel_settings/hotel_settings.json
deleted file mode 100644
index d9f5572549..0000000000
--- a/erpnext/hotels/doctype/hotel_settings/hotel_settings.json
+++ /dev/null
@@ -1,175 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 1,
- "creation": "2017-12-08 17:50:24.523107",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "default_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": "Default 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": "default_taxes_and_charges",
- "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": "Default Taxes and Charges",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Taxes and Charges Template",
- "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": "default_invoice_naming_series",
- "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": "Default Invoice Naming Series",
- "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": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-09 12:11:12.857308",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "Hotel Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/hotels/doctype/hotel_settings/hotel_settings.py b/erpnext/hotels/doctype/hotel_settings/hotel_settings.py
deleted file mode 100644
index 8376d50969..0000000000
--- a/erpnext/hotels/doctype/hotel_settings/hotel_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class HotelSettings(Document):
- pass
diff --git a/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.py b/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.py
deleted file mode 100644
index e76c00ce10..0000000000
--- a/erpnext/hotels/doctype/hotel_settings/test_hotel_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestHotelSettings(unittest.TestCase):
- pass
diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.js b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.js
deleted file mode 100644
index 81efb2dd12..0000000000
--- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-/* eslint-disable */
-
-frappe.query_reports["Hotel Room Occupancy"] = {
- "filters": [
- {
- "fieldname":"from_date",
- "label": __("From Date"),
- "fieldtype": "Date",
- "default": frappe.datetime.now_date(),
- "reqd":1
- },
- {
- "fieldname":"to_date",
- "label": __("To Date"),
- "fieldtype": "Date",
- "default": frappe.datetime.now_date(),
- "reqd":1
- }
- ]
-}
diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.json b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.json
deleted file mode 100644
index 782a48bbb9..0000000000
--- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "add_total_row": 1,
- "apply_user_permissions": 1,
- "creation": "2017-12-09 14:31:26.306705",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 0,
- "is_standard": "Yes",
- "modified": "2017-12-09 14:31:26.306705",
- "modified_by": "Administrator",
- "module": "Hotels",
- "name": "Hotel Room Occupancy",
- "owner": "Administrator",
- "ref_doctype": "Hotel Room Reservation",
- "report_name": "Hotel Room Occupancy",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "System Manager"
- },
- {
- "role": "Hotel Reservation User"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py
deleted file mode 100644
index c43589d2a8..0000000000
--- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.utils import add_days, date_diff
-
-from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import get_rooms_booked
-
-
-def execute(filters=None):
- columns = get_columns(filters)
- data = get_data(filters)
- return columns, data
-
-def get_columns(filters):
- columns = [
- dict(label=_("Room Type"), fieldname="room_type"),
- dict(label=_("Rooms Booked"), fieldtype="Int")
- ]
- return columns
-
-def get_data(filters):
- out = []
- for room_type in frappe.get_all('Hotel Room Type'):
- total_booked = 0
- for i in range(date_diff(filters.to_date, filters.from_date)):
- day = add_days(filters.from_date, i)
- total_booked += get_rooms_booked(room_type.name, day)
-
- out.append([room_type.name, total_booked])
-
- return out
diff --git a/erpnext/hr/doctype/appointment_letter/appointment_letter.json b/erpnext/hr/doctype/appointment_letter/appointment_letter.json
index c81b7004f6..012f6b6b49 100644
--- a/erpnext/hr/doctype/appointment_letter/appointment_letter.json
+++ b/erpnext/hr/doctype/appointment_letter/appointment_letter.json
@@ -86,11 +86,12 @@
}
],
"links": [],
- "modified": "2020-01-21 17:30:36.334395",
+ "modified": "2022-01-18 19:27:35.649424",
"modified_by": "Administrator",
"module": "HR",
"name": "Appointment Letter",
"name_case": "Title Case",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -118,7 +119,10 @@
"write": 1
}
],
+ "search_fields": "applicant_name, company",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "applicant_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json b/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json
index c136fb22fa..5e50fe6d8f 100644
--- a/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json
+++ b/erpnext/hr/doctype/appointment_letter_template/appointment_letter_template.json
@@ -1,11 +1,12 @@
{
"actions": [],
- "autoname": "HR-APP-LETTER-TEMP-.#####",
+ "autoname": "field:template_name",
"creation": "2019-12-26 12:20:14.219578",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "template_name",
"introduction",
"terms",
"closing_notes"
@@ -29,13 +30,21 @@
"label": "Terms",
"options": "Appointment Letter content",
"reqd": 1
+ },
+ {
+ "fieldname": "template_name",
+ "fieldtype": "Data",
+ "label": "Template Name",
+ "reqd": 1,
+ "unique": 1
}
],
"links": [],
- "modified": "2020-01-21 17:00:46.779420",
+ "modified": "2022-01-18 19:25:14.614616",
"modified_by": "Administrator",
"module": "HR",
"name": "Appointment Letter Template",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -63,7 +72,10 @@
"write": 1
}
],
+ "search_fields": "template_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "template_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 7dcfac249f..b1eaaf8b58 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -5,9 +5,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate
+from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
-from erpnext.hr.utils import validate_active_employee
+from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
class Attendance(Document):
@@ -171,7 +171,7 @@ def get_month_map():
})
@frappe.whitelist()
-def get_unmarked_days(employee, month):
+def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
month_map = get_month_map()
@@ -191,6 +191,11 @@ def get_unmarked_days(employee, month):
])
marked_days = [get_datetime(record.attendance_date) for record in records]
+ if cint(exclude_holidays):
+ holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
+ holidays = [get_datetime(record) for record in holiday_dates]
+ marked_days.extend(holidays)
+
unmarked_days = []
for date in dates_of_month:
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 6b3c29a76b..3a5c591539 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = {
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
+ dialog.set_df_property("exclude_holidays", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
@@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = {
onchange: function() {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
+ dialog.set_df_property("exclude_holidays", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
- me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
+ me.get_multi_select_options(
+ dialog.fields_dict.employee.value,
+ dialog.fields_dict.month.value,
+ dialog.fields_dict.exclude_holidays.get_value()
+ ).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
@@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = {
reqd: 1,
},
+ {
+ label: __("Exclude Holidays"),
+ fieldtype: "Check",
+ fieldname: "exclude_holidays",
+ hidden: 1,
+ onchange: function() {
+ if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
+ dialog.set_df_property("status", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
+ me.get_multi_select_options(
+ dialog.fields_dict.employee.value,
+ dialog.fields_dict.month.value,
+ dialog.fields_dict.exclude_holidays.get_value()
+ ).then(options => {
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
+ });
+ }
+ }
+ },
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
@@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = {
});
},
- get_multi_select_options: function(employee, month) {
+ get_multi_select_options: function(employee, month, exclude_holidays) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
@@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = {
args: {
employee: employee,
month: month,
+ exclude_holidays: exclude_holidays
}
}).then(r => {
var options = [];
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index a1247d9eb1..fb62b0414d 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -21,7 +21,7 @@ def get_data():
},
{
'label': _('Lifecycle'),
- 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
+ 'items': ['Employee Onboarding', 'Employee Transfer', 'Employee Promotion', 'Employee Grievance']
},
{
'label': _('Exit'),
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 559bd393e6..0bb66374d1 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -20,6 +20,7 @@ def send_reminders_in_advance_weekly():
send_advance_holiday_reminders("Weekly")
+
def send_reminders_in_advance_monthly():
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
frequency = frappe.db.get_single_value("HR Settings", "frequency")
@@ -28,6 +29,7 @@ def send_reminders_in_advance_monthly():
send_advance_holiday_reminders("Monthly")
+
def send_advance_holiday_reminders(frequency):
"""Send Holiday Reminders in Advance to Employees
`frequency` (str): 'Weekly' or 'Monthly'
@@ -42,7 +44,7 @@ def send_advance_holiday_reminders(frequency):
else:
return
- employees = frappe.db.get_all('Employee', pluck='name')
+ employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name')
for employee in employees:
holidays = get_holidays_for_employee(
employee,
@@ -51,10 +53,13 @@ def send_advance_holiday_reminders(frequency):
raise_exception=False
)
- if not (holidays is None):
- send_holidays_reminder_in_advance(employee, holidays)
+ send_holidays_reminder_in_advance(employee, holidays)
+
def send_holidays_reminder_in_advance(employee, holidays):
+ if not holidays:
+ return
+
employee_doc = frappe.get_doc('Employee', employee)
employee_email = get_employee_email(employee_doc)
frequency = frappe.db.get_single_value("HR Settings", "frequency")
@@ -101,6 +106,7 @@ def send_birthday_reminders():
reminder_text, message = get_birthday_reminder_text_and_message(others)
send_birthday_reminder(person_email, reminder_text, others, message)
+
def get_birthday_reminder_text_and_message(birthday_persons):
if len(birthday_persons) == 1:
birthday_person_text = birthday_persons[0]['name']
@@ -116,6 +122,7 @@ def get_birthday_reminder_text_and_message(birthday_persons):
return reminder_text, message
+
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
frappe.sendmail(
recipients=recipients,
@@ -129,10 +136,12 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
header=_("Birthday Reminder 🎂")
)
+
def get_employees_who_are_born_today():
"""Get all employee born today & group them based on their company"""
return get_employees_having_an_event_today("birthday")
+
def get_employees_having_an_event_today(event_type):
"""Get all employee who have `event_type` today
& group them based on their company. `event_type`
@@ -210,13 +219,14 @@ def send_work_anniversary_reminders():
reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
send_work_anniversary_reminder(person_email, reminder_text, others, message)
+
def get_work_anniversary_reminder_text_and_message(anniversary_persons):
if len(anniversary_persons) == 1:
anniversary_person = anniversary_persons[0]['name']
persons_name = anniversary_person
# Number of years completed at the company
completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
- anniversary_person += f" completed {completed_years} years"
+ anniversary_person += f" completed {completed_years} year(s)"
else:
person_names_with_years = []
names = []
@@ -225,7 +235,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
names.append(person_text)
# Number of years completed at the company
completed_years = getdate().year - person['date_of_joining'].year
- person_text += f" completed {completed_years} years"
+ person_text += f" completed {completed_years} year(s)"
person_names_with_years.append(person_text)
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
@@ -239,6 +249,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
return reminder_text, message
+
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
frappe.sendmail(
recipients=recipients,
@@ -249,5 +260,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person
anniversary_persons=anniversary_persons,
message=message,
),
- header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️")
+ header=_("Work Anniversary Reminder")
)
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8a2da0866e..67cbea67e1 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -36,7 +36,7 @@ class TestEmployee(unittest.TestCase):
employee_doc.reload()
make_holiday_list()
- frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
+ frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py
index 52c0098244..a4097ab9d1 100644
--- a/erpnext/hr/doctype/employee/test_employee_reminders.py
+++ b/erpnext/hr/doctype/employee/test_employee_reminders.py
@@ -5,10 +5,12 @@ import unittest
from datetime import timedelta
import frappe
-from frappe.utils import getdate
+from frappe.utils import add_months, getdate
+from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
+from erpnext.hr.utils import get_holidays_for_employee
class TestEmployeeReminders(unittest.TestCase):
@@ -46,6 +48,24 @@ class TestEmployeeReminders(unittest.TestCase):
cls.test_employee = test_employee
cls.test_holiday_dates = test_holiday_dates
+ # Employee without holidays in this month/week
+ test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company")
+ test_employee_2 = frappe.get_doc('Employee', test_employee_2)
+
+ test_holiday_list = make_holiday_list(
+ 'TestHolidayRemindersList2',
+ holiday_dates=[
+ {'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'},
+ ],
+ from_date=add_months(getdate(), -2),
+ to_date=add_months(getdate(), 2)
+ )
+ test_employee_2.holiday_list = test_holiday_list.name
+ test_employee_2.save()
+
+ cls.test_employee_2 = test_employee_2
+ cls.holiday_list_2 = test_holiday_list
+
@classmethod
def get_test_holiday_dates(cls):
today_date = getdate()
@@ -61,6 +81,7 @@ class TestEmployeeReminders(unittest.TestCase):
def setUp(self):
# Clear Email Queue
frappe.db.sql("delete from `tabEmail Queue`")
+ frappe.db.sql("delete from `tabEmail Queue Recipient`")
def test_is_holiday(self):
from erpnext.hr.doctype.employee.employee import is_holiday
@@ -103,11 +124,10 @@ class TestEmployeeReminders(unittest.TestCase):
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
def test_work_anniversary_reminders(self):
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
- employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:]
- employee.company_email = "test@example.com"
- employee.company = "_Test Company"
- employee.save()
+ make_employee("test_work_anniversary@gmail.com",
+ date_of_joining="1998" + frappe.utils.nowdate()[4:],
+ company="_Test Company",
+ )
from erpnext.hr.doctype.employee.employee_reminders import (
get_employees_having_an_event_today,
@@ -115,7 +135,12 @@ class TestEmployeeReminders(unittest.TestCase):
)
employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
- self.assertTrue(employees_having_work_anniversary.get("_Test Company"))
+ employees = employees_having_work_anniversary.get("_Test Company") or []
+ user_ids = []
+ for entry in employees:
+ user_ids.append(entry.user_id)
+
+ self.assertTrue("test_work_anniversary@gmail.com" in user_ids)
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_work_anniversary_reminders = 1
@@ -126,16 +151,24 @@ class TestEmployeeReminders(unittest.TestCase):
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
- def test_send_holidays_reminder_in_advance(self):
- from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
- from erpnext.hr.utils import get_holidays_for_employee
+ def test_work_anniversary_reminder_not_sent_for_0_years(self):
+ make_employee("test_work_anniversary_2@gmail.com",
+ date_of_joining=getdate(),
+ company="_Test Company",
+ )
- # Get HR settings and enable advance holiday reminders
- hr_settings = frappe.get_doc("HR Settings", "HR Settings")
- hr_settings.send_holiday_reminders = 1
- set_proceed_with_frequency_change()
- hr_settings.frequency = 'Weekly'
- hr_settings.save()
+ from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today
+
+ employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
+ employees = employees_having_work_anniversary.get("_Test Company") or []
+ user_ids = []
+ for entry in employees:
+ user_ids.append(entry.user_id)
+
+ self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids)
+
+ def test_send_holidays_reminder_in_advance(self):
+ setup_hr_settings('Weekly')
holidays = get_holidays_for_employee(
self.test_employee.get('name'),
@@ -151,32 +184,80 @@ class TestEmployeeReminders(unittest.TestCase):
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertEqual(len(email_queue), 1)
+ self.assertTrue("Holidays this Week." in email_queue[0].message)
def test_advance_holiday_reminders_monthly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
- # Get HR settings and enable advance holiday reminders
- hr_settings = frappe.get_doc("HR Settings", "HR Settings")
- hr_settings.send_holiday_reminders = 1
- set_proceed_with_frequency_change()
- hr_settings.frequency = 'Monthly'
- hr_settings.save()
+ setup_hr_settings('Monthly')
+
+ # disable emp 2, set same holiday list
+ frappe.db.set_value('Employee', self.test_employee_2.name, {
+ 'status': 'Left',
+ 'holiday_list': self.test_employee.holiday_list
+ })
send_reminders_in_advance_monthly()
-
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0)
+ # even though emp 2 has holiday, non-active employees should not be recipients
+ recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
+ self.assertTrue(self.test_employee_2.user_id not in recipients)
+
+ # teardown: enable emp 2
+ frappe.db.set_value('Employee', self.test_employee_2.name, {
+ 'status': 'Active',
+ 'holiday_list': self.holiday_list_2.name
+ })
+
def test_advance_holiday_reminders_weekly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
- # Get HR settings and enable advance holiday reminders
- hr_settings = frappe.get_doc("HR Settings", "HR Settings")
- hr_settings.send_holiday_reminders = 1
- hr_settings.frequency = 'Weekly'
- hr_settings.save()
+ setup_hr_settings('Weekly')
+
+ # disable emp 2, set same holiday list
+ frappe.db.set_value('Employee', self.test_employee_2.name, {
+ 'status': 'Left',
+ 'holiday_list': self.test_employee.holiday_list
+ })
send_reminders_in_advance_weekly()
-
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0)
+
+ # even though emp 2 has holiday, non-active employees should not be recipients
+ recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
+ self.assertTrue(self.test_employee_2.user_id not in recipients)
+
+ # teardown: enable emp 2
+ frappe.db.set_value('Employee', self.test_employee_2.name, {
+ 'status': 'Active',
+ 'holiday_list': self.holiday_list_2.name
+ })
+
+ def test_reminder_not_sent_if_no_holdays(self):
+ setup_hr_settings('Monthly')
+
+ # reminder not sent if there are no holidays
+ holidays = get_holidays_for_employee(
+ self.test_employee_2.get('name'),
+ getdate(), getdate() + timedelta(days=3),
+ only_non_weekly=True,
+ raise_exception=False
+ )
+ send_holidays_reminder_in_advance(
+ self.test_employee_2.get('name'),
+ holidays
+ )
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertEqual(len(email_queue), 0)
+
+
+def setup_hr_settings(frequency=None):
+ # Get HR settings and enable advance holiday reminders
+ hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+ hr_settings.send_holiday_reminders = 1
+ set_proceed_with_frequency_change()
+ hr_settings.frequency = frequency or 'Weekly'
+ hr_settings.save()
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json b/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json
index 044a5a9886..8474bd09d5 100644
--- a/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json
+++ b/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json
@@ -62,6 +62,7 @@
},
{
"default": "0",
+ "depends_on": "eval:['Employee Onboarding', 'Employee Onboarding Template'].includes(doc.parenttype)",
"description": "Applicable in the case of Employee Onboarding",
"fieldname": "required_for_employee_creation",
"fieldtype": "Check",
@@ -93,7 +94,7 @@
],
"istable": 1,
"links": [],
- "modified": "2021-07-30 15:55:22.470102",
+ "modified": "2022-01-29 14:05:00.543122",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Boarding Activity",
@@ -102,5 +103,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
index 5d1a024ebb..6fbb54d002 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
@@ -3,12 +3,6 @@
frappe.ui.form.on('Employee Onboarding', {
setup: function(frm) {
- frm.add_fetch("employee_onboarding_template", "company", "company");
- frm.add_fetch("employee_onboarding_template", "department", "department");
- frm.add_fetch("employee_onboarding_template", "designation", "designation");
- frm.add_fetch("employee_onboarding_template", "employee_grade", "employee_grade");
-
-
frm.set_query("job_applicant", function () {
return {
filters:{
@@ -71,5 +65,19 @@ frappe.ui.form.on('Employee Onboarding', {
}
});
}
+ },
+
+ job_applicant: function(frm) {
+ if (frm.doc.job_applicant) {
+ frappe.db.get_value('Employee', {'job_applicant': frm.doc.job_applicant}, 'name', (r) => {
+ if (r.name) {
+ frm.set_value('employee', r.name);
+ } else {
+ frm.set_value('employee', '');
+ }
+ });
+ } else {
+ frm.set_value('employee', '');
+ }
}
});
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
index fd877a68d8..1d2ea0c669 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
@@ -92,6 +92,7 @@
"options": "Employee Onboarding Template"
},
{
+ "fetch_from": "employee_onboarding_template.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -99,6 +100,7 @@
"reqd": 1
},
{
+ "fetch_from": "employee_onboarding_template.department",
"fieldname": "department",
"fieldtype": "Link",
"in_list_view": 1,
@@ -106,6 +108,7 @@
"options": "Department"
},
{
+ "fetch_from": "employee_onboarding_template.designation",
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
@@ -113,6 +116,7 @@
"options": "Designation"
},
{
+ "fetch_from": "employee_onboarding_template.employee_grade",
"fieldname": "employee_grade",
"fieldtype": "Link",
"label": "Employee Grade",
@@ -170,10 +174,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-07-30 14:55:04.560683",
+ "modified": "2022-01-29 12:33:57.120384",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Onboarding",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -194,6 +199,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
index eba2a03193..a0939a847b 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
@@ -14,10 +14,15 @@ class IncompleteTaskError(frappe.ValidationError): pass
class EmployeeOnboarding(EmployeeBoardingController):
def validate(self):
super(EmployeeOnboarding, self).validate()
+ self.set_employee()
self.validate_duplicate_employee_onboarding()
+ def set_employee(self):
+ if not self.employee:
+ self.employee = frappe.db.get_value('Employee', {'job_applicant': self.job_applicant}, 'name')
+
def validate_duplicate_employee_onboarding(self):
- emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant})
+ emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant, "docstatus": ("!=", 2)})
if emp_onboarding and emp_onboarding != self.name:
frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py
index 4bb003ded1..a3b111ccb1 100644
--- a/erpnext/hr/doctype/interview/interview.py
+++ b/erpnext/hr/doctype/interview/interview.py
@@ -7,7 +7,7 @@ import datetime
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cstr, get_datetime, get_link_to_form
+from frappe.utils import cstr, flt, get_datetime, get_link_to_form
class DuplicateInterviewRoundError(frappe.ValidationError):
@@ -18,6 +18,7 @@ class Interview(Document):
self.validate_duplicate_interview()
self.validate_designation()
self.validate_overlap()
+ self.set_average_rating()
def on_submit(self):
if self.status not in ['Cleared', 'Rejected']:
@@ -67,6 +68,13 @@ class Interview(Document):
overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
frappe.throw(overlapping_details, title=_('Overlap'))
+ def set_average_rating(self):
+ total_rating = 0
+ for entry in self.interview_details:
+ if entry.average_rating:
+ total_rating += entry.average_rating
+
+ self.average_rating = flt(total_rating / len(self.interview_details) if len(self.interview_details) else 0)
@frappe.whitelist()
def reschedule_interview(self, scheduled_on, from_time, to_time):
diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py
index 1a2257a6d9..fdb11afe82 100644
--- a/erpnext/hr/doctype/interview/test_interview.py
+++ b/erpnext/hr/doctype/interview/test_interview.py
@@ -12,6 +12,7 @@ from frappe.utils import add_days, getdate, nowtime
from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
+from erpnext.hr.doctype.job_applicant.job_applicant import get_interview_details
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
@@ -70,6 +71,20 @@ class TestInterview(unittest.TestCase):
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
+ def test_get_interview_details_for_applicant_dashboard(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name)
+
+ details = get_interview_details(job_applicant.name)
+ self.assertEqual(details.get('stars'), 5)
+ self.assertEqual(details.get('interviews').get(interview.name), {
+ 'name': interview.name,
+ 'interview_round': interview.interview_round,
+ 'expected_average_rating': interview.expected_average_rating * 5,
+ 'average_rating': interview.average_rating * 5,
+ 'status': 'Pending'
+ })
+
def tearDown(self):
frappe.db.rollback()
@@ -106,7 +121,8 @@ def create_interview_round(name, skill_set, interviewers=[], designation=None, s
interview_round = frappe.new_doc("Interview Round")
interview_round.round_name = name
interview_round.interview_type = create_interview_type()
- interview_round.expected_average_rating = 4
+ # average rating = 4
+ interview_round.expected_average_rating = 0.8
if designation:
interview_round.designation = designation
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
index d046458f19..2ff00c1cac 100644
--- a/erpnext/hr/doctype/interview_feedback/interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
@@ -57,7 +57,6 @@ class InterviewFeedback(Document):
def update_interview_details(self):
doc = frappe.get_doc('Interview', self.interview)
- total_rating = 0
if self.docstatus == 2:
for entry in doc.interview_details:
@@ -72,10 +71,6 @@ class InterviewFeedback(Document):
entry.comments = self.feedback
entry.result = self.result
- if entry.average_rating:
- total_rating += entry.average_rating
-
- doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
doc.save()
doc.notify_update()
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
index d2ec5b9438..19c464296a 100644
--- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -24,7 +24,7 @@ class TestInterviewFeedback(unittest.TestCase):
create_skill_set(['Leadership'])
interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
- interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
+ interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 0.8})
frappe.set_user(interviewer)
self.assertRaises(frappe.ValidationError, interview_feedback.save)
@@ -50,7 +50,7 @@ class TestInterviewFeedback(unittest.TestCase):
avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
- self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
+ self.assertEqual(flt(avg_rating, 2), flt(feedback_1.average_rating, 2))
avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
'parent': feedback_1.interview,
@@ -59,7 +59,7 @@ class TestInterviewFeedback(unittest.TestCase):
}, 'average_rating')
# 1. average should be reflected in Interview Detail.
- self.assertEqual(avg_on_interview_detail, feedback_1.average_rating)
+ self.assertEqual(flt(avg_on_interview_detail, 2), flt(feedback_1.average_rating, 2))
'''For Second Interviewer Feedback'''
interviewer = interview.interview_details[1].interviewer
@@ -97,5 +97,5 @@ def get_skills_rating(interview_round):
skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
for d in skills:
- d["rating"] = random.randint(1, 5)
+ d["rating"] = random.random()
return skills
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js
index d7b1c6c9df..c1e8257168 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.js
@@ -21,9 +21,9 @@ frappe.ui.form.on("Job Applicant", {
create_custom_buttons: function(frm) {
if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
- frm.add_custom_button(__("Create Interview"), function() {
+ frm.add_custom_button(__("Interview"), function() {
frm.events.create_dialog(frm);
- });
+ }, __("Create"));
}
if (!frm.doc.__islocal) {
@@ -40,10 +40,10 @@ frappe.ui.form.on("Job Applicant", {
frappe.route_options = {
"job_applicant": frm.doc.name,
"applicant_name": frm.doc.applicant_name,
- "designation": frm.doc.job_opening,
+ "designation": frm.doc.job_opening || frm.doc.designation,
};
frappe.new_doc("Job Offer");
- });
+ }, __("Create"));
}
}
},
@@ -55,13 +55,16 @@ frappe.ui.form.on("Job Applicant", {
job_applicant: frm.doc.name
},
callback: function(r) {
- $("div").remove(".form-dashboard-section.custom");
- frm.dashboard.add_section(
- frappe.render_template('job_applicant_dashboard', {
- data: r.message
- }),
- __("Interview Summary")
- );
+ if (r.message) {
+ $("div").remove(".form-dashboard-section.custom");
+ frm.dashboard.add_section(
+ frappe.render_template("job_applicant_dashboard", {
+ data: r.message.interviews,
+ number_of_stars: r.message.stars
+ }),
+ __("Interview Summary")
+ );
+ }
}
});
},
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index 5b3d9bfb4f..ccc21ced2c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -81,8 +81,13 @@ def get_interview_details(job_applicant):
fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
)
interview_detail_map = {}
+ meta = frappe.get_meta("Interview")
+ number_of_stars = meta.get_options("expected_average_rating") or 5
for detail in interview_details:
+ detail.expected_average_rating = detail.expected_average_rating * number_of_stars if detail.expected_average_rating else 0
+ detail.average_rating = detail.average_rating * number_of_stars if detail.average_rating else 0
+
interview_detail_map[detail.name] = detail
- return interview_detail_map
+ return {"interviews": interview_detail_map, "stars": number_of_stars}
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
index c286787a55..734b2fe5e8 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
@@ -17,24 +17,33 @@
{%= key %}
{%= value["interview_round"] %}
{%= value["status"] %}
-
- {% for (i = 0; i < value["expected_average_rating"]; i++) { %}
-
- {% } %}
- {% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %}
-
- {% } %}
-
-
- {% if(value["average_rating"]){ %}
- {% for (i = 0; i < value["average_rating"]; i++) { %}
-
- {% } %}
- {% for (i = 0; i < (5-value["average_rating"]); i++) { %}
-
- {% } %}
- {% } %}
-
+ {% let right_class = ''; %}
+ {% let left_class = ''; %}
+
+ {% $.each([value["expected_average_rating"], value["average_rating"]], (_, val) => { %}
+
+
+ {% for (let i = 1; i <= number_of_stars; i++) { %}
+ {% if (i <= val) { %}
+ {% right_class = 'star-click'; %}
+ {% } else { %}
+ {% right_class = ''; %}
+ {% } %}
+
+ {% if ((i <= val) || ((i - 0.5) == val)) { %}
+ {% left_class = 'star-click'; %}
+ {% } else { %}
+ {% left_class = ''; %}
+ {% } %}
+
+
+
+
+
+ {% } %}
+
+
+ {% }); %}
{% } %}
diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py
index 39f471929b..072fc73271 100644
--- a/erpnext/hr/doctype/job_offer/job_offer.py
+++ b/erpnext/hr/doctype/job_offer/job_offer.py
@@ -78,6 +78,7 @@ def make_employee(source_name, target_doc=None):
"doctype": "Employee",
"field_map": {
"applicant_name": "employee_name",
+ "offer_date": "scheduled_confirmation_date"
}}
}, target_doc, set_missing_values)
return doc
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 52ee463db0..9ecbe014b9 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -237,10 +237,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-01 15:28:26.335104",
+ "modified": "2022-01-18 19:15:53.262536",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -278,5 +279,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "timeline_field": "employee"
-}
+ "states": [],
+ "timeline_field": "employee",
+ "title_field": "employee_name"
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 1dc5b31461..70250f5bcf 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -22,6 +22,7 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.utils import (
+ get_holiday_dates_for_employee,
get_leave_period,
set_employee_name,
share_doc_with_approver,
@@ -159,33 +160,57 @@ class LeaveApplication(Document):
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name))
def update_attendance(self):
- if self.status == "Approved":
- for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
- date = dt.strftime("%Y-%m-%d")
- status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
- attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
- attendance_date = date, docstatus = ('!=', 2)))
+ if self.status != "Approved":
+ return
+ holiday_dates = []
+ if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"):
+ holiday_dates = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date)
+
+ for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
+ date = dt.strftime("%Y-%m-%d")
+ attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee,
+ attendance_date = date, docstatus = ('!=', 2)))
+
+ # don't mark attendance for holidays
+ # if leave type does not include holidays within leaves as leaves
+ if date in holiday_dates:
if attendance_name:
- # update existing attendance, change absent to on leave
- doc = frappe.get_doc('Attendance', attendance_name)
- if doc.status != status:
- doc.db_set('status', status)
- doc.db_set('leave_type', self.leave_type)
- doc.db_set('leave_application', self.name)
- else:
- # make new attendance and submit it
- doc = frappe.new_doc("Attendance")
- doc.employee = self.employee
- doc.employee_name = self.employee_name
- doc.attendance_date = date
- doc.company = self.company
- doc.leave_type = self.leave_type
- doc.leave_application = self.name
- doc.status = status
- doc.flags.ignore_validate = True
- doc.insert(ignore_permissions=True)
- doc.submit()
+ # cancel and delete existing attendance for holidays
+ attendance = frappe.get_doc("Attendance", attendance_name)
+ attendance.flags.ignore_permissions = True
+ if attendance.docstatus == 1:
+ attendance.cancel()
+ frappe.delete_doc("Attendance", attendance_name, force=1)
+ continue
+
+ self.create_or_update_attendance(attendance_name, date)
+
+ def create_or_update_attendance(self, attendance_name, date):
+ status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
+
+ if attendance_name:
+ # update existing attendance, change absent to on leave
+ doc = frappe.get_doc('Attendance', attendance_name)
+ if doc.status != status:
+ doc.db_set({
+ 'status': status,
+ 'leave_type': self.leave_type,
+ 'leave_application': self.name
+ })
+ else:
+ # make new attendance and submit it
+ doc = frappe.new_doc("Attendance")
+ doc.employee = self.employee
+ doc.employee_name = self.employee_name
+ doc.attendance_date = date
+ doc.company = self.company
+ doc.leave_type = self.leave_type
+ doc.leave_application = self.name
+ doc.status = status
+ doc.flags.ignore_validate = True
+ doc.insert(ignore_permissions=True)
+ doc.submit()
def cancel_attendance(self):
if self.docstatus == 2:
diff --git a/erpnext/hr/doctype/leave_application/leave_application_email_template.html b/erpnext/hr/doctype/leave_application/leave_application_email_template.html
index 14ca41bebc..dae9084f79 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_email_template.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_email_template.html
@@ -23,3 +23,8 @@
{{status}}
+
+ {% set doc_link = frappe.utils.get_url_to_form('Leave Application', name) %}
+
+
+
{{ _('Open Now') }}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index f73d3e52da..6b85927d3e 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -5,7 +5,16 @@ import unittest
import frappe
from frappe.permissions import clear_user_permissions_for_doctype
-from frappe.utils import add_days, add_months, getdate, nowdate
+from frappe.utils import (
+ add_days,
+ add_months,
+ get_first_day,
+ get_last_day,
+ get_year_ending,
+ get_year_start,
+ getdate,
+ nowdate,
+)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
@@ -19,6 +28,10 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
)
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
@@ -61,13 +74,13 @@ class TestLeaveApplication(unittest.TestCase):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
- @classmethod
- def setUpClass(cls):
+ frappe.set_user("Administrator")
set_leave_approver()
+
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self):
- frappe.set_user("Administrator")
+ frappe.db.rollback()
def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -106,6 +119,76 @@ class TestLeaveApplication(unittest.TestCase):
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates)
+ def test_attendance_for_include_holidays(self):
+ # Case 1: leave type with 'Include holidays within leaves as leaves' enabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Include Holidays",
+ doctype="Leave Type",
+ include_holiday=True
+ )).insert()
+
+ date = getdate()
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ holiday_list = make_holiday_list()
+ employee = get_employee()
+ frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
+ first_sunday = get_first_sunday(holiday_list)
+
+ leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ self.assertEqual(leave_application.total_leave_days, 4)
+ self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
+
+ leave_application.cancel()
+
+ def test_attendance_update_for_exclude_holidays(self):
+ # Case 2: leave type with 'Include holidays within leaves as leaves' disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
+ leave_type = frappe.get_doc(dict(
+ leave_type_name="Test Do Not Include Holidays",
+ doctype="Leave Type",
+ include_holiday=False
+ )).insert()
+
+ date = getdate()
+ make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+
+ holiday_list = make_holiday_list()
+ employee = get_employee()
+ frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
+ first_sunday = get_first_sunday(holiday_list)
+
+ # already marked attendance on a holiday should be deleted in this case
+ config = {
+ "doctype": "Attendance",
+ "employee": employee.name,
+ "status": "Present"
+ }
+ attendance_on_holiday = frappe.get_doc(config)
+ attendance_on_holiday.attendance_date = first_sunday
+ attendance_on_holiday.flags.ignore_validate = True
+ attendance_on_holiday.save()
+
+ # already marked attendance on a non-holiday should be updated
+ attendance = frappe.get_doc(config)
+ attendance.attendance_date = add_days(first_sunday, 3)
+ attendance.flags.ignore_validate = True
+ attendance.save()
+
+ leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application.reload()
+ # holiday should be excluded while marking attendance
+ self.assertEqual(leave_application.total_leave_days, 3)
+ self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
+
+ # attendance on holiday deleted
+ self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name))
+
+ # attendance on non-holiday updated
+ self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
+
def test_block_list(self):
self._clear_roles()
@@ -241,7 +324,13 @@ class TestLeaveApplication(unittest.TestCase):
leave_period = get_leave_period()
today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday'
- optional_leave_date = add_days(today, 7)
+ employee = get_employee()
+
+ default_holiday_list = make_holiday_list()
+ frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
+ first_sunday = get_first_sunday(default_holiday_list)
+
+ optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
@@ -253,7 +342,6 @@ class TestLeaveApplication(unittest.TestCase):
dict(holiday_date = optional_leave_date, description = 'Test')
]
)).insert()
- employee = get_employee()
frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list)
leave_type = 'Test Optional Type'
@@ -266,7 +354,7 @@ class TestLeaveApplication(unittest.TestCase):
allocate_leaves(employee, leave_period, leave_type, 10)
- date = add_days(today, 6)
+ date = add_days(first_sunday, 2)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
@@ -443,6 +531,7 @@ class TestLeaveApplication(unittest.TestCase):
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
+ "title": "Test Leave Policy",
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}]
}).insert()
@@ -636,13 +725,13 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1)
leave_allocation.submit()
-def make_allocation_record(employee=None, leave_type=None):
+def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type",
- "from_date": "2013-01-01",
- "to_date": "2019-12-31",
+ "from_date": from_date or "2013-01-01",
+ "to_date": to_date or "2019-12-31",
"new_leaves_allocated": 30
})
@@ -691,3 +780,16 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
}).insert()
allocate_leave.submit()
+
+
+def get_first_sunday(holiday_list):
+ month_start_date = get_first_day(nowdate())
+ month_end_date = get_last_day(nowdate())
+ first_sunday = frappe.db.sql("""
+ select holiday_date from `tabHoliday`
+ where parent = %s
+ and holiday_date between %s and %s
+ order by holiday_date
+ """, (holiday_list, month_start_date, month_end_date))[0][0]
+
+ return first_sunday
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json
index 1f6c03f7b6..cc4e53eb90 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json
@@ -154,10 +154,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:32:55.492327",
+ "modified": "2022-01-18 19:16:52.414356",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Encashment",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -218,7 +219,10 @@
"write": 1
}
],
+ "search_fields": "employee,employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy.json b/erpnext/hr/doctype/leave_policy/leave_policy.json
index 373095d075..6ac8f20ea2 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy.json
+++ b/erpnext/hr/doctype/leave_policy/leave_policy.json
@@ -1,131 +1,55 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "HR-LPOL-.YYYY.-.#####",
- "beta": 0,
"creation": "2018-04-13 16:06:19.507624",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "title",
+ "leave_allocations_section",
+ "leave_policy_details",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
"allow_in_quick_entry": 1,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "leave_allocations_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": "Leave Allocations",
- "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
+ "label": "Leave Allocations"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "leave_policy_details",
"fieldtype": "Table",
- "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": "Leave Policy Details",
- "length": 0,
- "no_copy": 0,
"options": "Leave Policy Detail",
- "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,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "amended_from",
"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": "Amended From",
- "length": 0,
"no_copy": 1,
"options": "Leave Policy",
- "permlevel": 0,
"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
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
"is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-29 08:42:53.363088",
+ "links": [],
+ "modified": "2022-01-19 13:07:40.556500",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy",
- "name_case": "",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -135,14 +59,10 @@
"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": 1,
"write": 1
@@ -154,14 +74,10 @@
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
@@ -173,26 +89,19 @@
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
+ "search_fields": "title",
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "states": [],
+ "title_field": "title",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py
index 3dbbef857e..a4b8af759e 100644
--- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py
+++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py
@@ -24,6 +24,7 @@ def create_leave_policy(**args):
args = frappe._dict(args)
return frappe.get_doc({
"doctype": "Leave Policy",
+ "title": "Test Leave Policy",
"leave_policy_details": [{
"leave_type": args.leave_type or "_Test Leave Type",
"annual_allocation": args.annual_allocation or 10
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index 8953a51e8b..3b7f8ec822 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -108,6 +108,7 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
+ "title": "Test Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
}).insert()
diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.json b/erpnext/hr/doctype/training_feedback/training_feedback.json
index cd967d514f..ebf5a506f0 100644
--- a/erpnext/hr/doctype/training_feedback/training_feedback.json
+++ b/erpnext/hr/doctype/training_feedback/training_feedback.json
@@ -1,443 +1,144 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "HR-TRF-.YYYY.-.#####",
- "beta": 0,
- "creation": "2016-08-08 06:35:34.158568",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "autoname": "HR-TRF-.YYYY.-.#####",
+ "creation": "2016-08-08 06:35:34.158568",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "department",
+ "course",
+ "column_break_3",
+ "training_event",
+ "event_name",
+ "trainer_name",
+ "section_break_6",
+ "feedback",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "employee",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 1,
- "label": "Employee",
- "length": 0,
- "no_copy": 0,
- "options": "Employee",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.employee_name",
- "fieldname": "employee_name",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Employee Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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
- },
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Read Only",
+ "in_global_search": 1,
+ "label": "Employee Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.department",
- "fieldname": "department",
- "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": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "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
- },
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "training_event.course",
- "fieldname": "course",
- "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": "Course",
- "length": 0,
- "no_copy": 0,
- "options": "Course",
- "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
- },
+ "fetch_from": "training_event.course",
+ "fieldname": "course",
+ "fieldtype": "Link",
+ "label": "Course",
+ "options": "Course",
+ "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_3",
- "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_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "training_event",
- "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": 1,
- "label": "Training Event",
- "length": 0,
- "no_copy": 0,
- "options": "Training Event",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "training_event",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Training Event",
+ "options": "Training Event",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "training_event.event_name",
- "fieldname": "event_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": "Event Name",
- "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
- },
+ "fetch_from": "training_event.event_name",
+ "fieldname": "event_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Event Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "training_event.trainer_name",
- "fieldname": "trainer_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": "Trainer Name",
- "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
- },
+ "fetch_from": "training_event.trainer_name",
+ "fieldname": "trainer_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Trainer Name",
+ "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_6",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "feedback",
- "fieldtype": "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": "Feedback",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "feedback",
+ "fieldtype": "Text",
+ "label": "Feedback",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "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": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Training Feedback",
- "permlevel": 0,
- "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": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Training Feedback",
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-30 11:28:13.849860",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Training Feedback",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-01-18 19:32:20.805277",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Training Feedback",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "employee_name",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "search_fields": "employee_name, training_event, event_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/training_result/training_result.json b/erpnext/hr/doctype/training_result/training_result.json
index dd7abd7753..f28669e3c2 100644
--- a/erpnext/hr/doctype/training_result/training_result.json
+++ b/erpnext/hr/doctype/training_result/training_result.json
@@ -1,226 +1,83 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "autoname": "HR-TRR-.YYYY.-.#####",
- "beta": 0,
- "creation": "2016-11-04 02:13:48.407576",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "HR-TRR-.YYYY.-.#####",
+ "creation": "2016-11-04 02:13:48.407576",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "training_event",
+ "section_break_3",
+ "employees",
+ "amended_from",
+ "employee_emails"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "training_event",
- "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": "Training Event",
- "length": 0,
- "no_copy": 0,
- "options": "Training Event",
- "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,
- "translatable": 0,
+ "fieldname": "training_event",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Training Event",
+ "options": "Training Event",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_3",
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "employees",
- "fieldtype": "Table",
- "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": "Employees",
- "length": 0,
- "no_copy": 0,
- "options": "Training Result Employee",
- "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": "employees",
+ "fieldtype": "Table",
+ "label": "Employees",
+ "options": "Training Result Employee"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "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": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Training Result",
- "permlevel": 0,
- "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": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Training Result",
+ "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": "employee_emails",
- "fieldtype": "Small Text",
- "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": "Employee Emails",
- "length": 0,
- "no_copy": 0,
- "options": "Email",
- "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": "employee_emails",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Employee Emails",
+ "options": "Email"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:47.614563",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Training Result",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-01-18 19:31:44.900034",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Training Result",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "training_event",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "search_fields": "training_event",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "training_event"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/travel_request/travel_request.json b/erpnext/hr/doctype/travel_request/travel_request.json
index 441907c02d..7908e1a8ea 100644
--- a/erpnext/hr/doctype/travel_request/travel_request.json
+++ b/erpnext/hr/doctype/travel_request/travel_request.json
@@ -216,10 +216,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2019-12-12 18:42:26.451359",
+ "modified": "2022-01-18 19:19:33.678664",
"modified_by": "Administrator",
"module": "HR",
"name": "Travel Request",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -235,7 +236,10 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index 85e641c856..30cec1b4a8 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -5,7 +5,7 @@
"label": "Outgoing Salary"
}
],
- "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"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\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:48:58.322521",
"docstatus": 0,
"doctype": "Workspace",
@@ -1642,7 +1642,7 @@
"type": "Link"
}
],
- "modified": "2021-12-05 22:05:13.004462",
+ "modified": "2022-01-13 17:38:45.489128",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",
@@ -1651,7 +1651,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 14,
+ "sequence_id": 14.0,
"shortcuts": [
{
"color": "Green",
diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
index 3d07081215..b7b20d945d 100644
--- a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
+++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
@@ -70,7 +70,6 @@
{
"fieldname": "loan_repayment_entry",
"fieldtype": "Link",
- "hidden": 1,
"label": "Loan Repayment Entry",
"no_copy": 1,
"options": "Loan Repayment",
@@ -88,7 +87,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-03-14 20:47:11.725818",
+ "modified": "2022-01-31 14:50:14.823213",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Salary Slip Loan",
@@ -97,5 +96,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json
index 7deee0d461..b08a85e213 100644
--- a/erpnext/loan_management/workspace/loan_management/loan_management.json
+++ b/erpnext/loan_management/workspace/loan_management/loan_management.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Processes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Disbursement and Repayment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Security\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-12 16:35:55.299820",
"docstatus": 0,
"doctype": "Workspace",
@@ -238,7 +238,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:18:13.350905",
+ "modified": "2022-01-13 17:39:16.790152",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loans",
@@ -247,7 +247,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 16,
+ "sequence_id": 16.0,
"shortcuts": [
{
"color": "Green",
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 2ffae1a4f2..07d928c221 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
import frappe
from frappe import _, throw
from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate
@@ -306,13 +305,18 @@ class MaintenanceSchedule(TransactionBase):
return schedule.name
@frappe.whitelist()
-def update_serial_nos(s_id):
- serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no')
+def get_serial_nos_from_schedule(item_code, schedule=None):
+ serial_nos = []
+ if schedule:
+ serial_nos = frappe.db.get_value('Maintenance Schedule Item', {
+ 'parent': schedule,
+ 'item_code': item_code
+ }, 'serial_no')
+
if serial_nos:
serial_nos = get_serial_nos(serial_nos)
- return serial_nos
- else:
- return False
+
+ return serial_nos
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
@@ -320,12 +324,9 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled"
- target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
- def update_sales_and_serial(source, target, parent):
- sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
- target.service_person = sales_person
+ def update_serial(source, target, parent):
serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
@@ -346,7 +347,10 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name,
- "postprocess": update_sales_and_serial
+ "field_map": {
+ "sales_person": "service_person"
+ },
+ "postprocess": update_serial
}
}, target_doc)
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index 501712613a..6e727e53ef 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -4,11 +4,15 @@
import unittest
import frappe
+from frappe.utils import format_date
from frappe.utils.data import add_days, formatdate, today
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import (
+ get_serial_nos_from_schedule,
make_maintenance_visit,
)
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
# test_records = frappe.get_test_records('Maintenance Schedule')
@@ -79,6 +83,49 @@ class TestMaintenanceSchedule(unittest.TestCase):
#checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
+ self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date))
+
+ #checks if visit status is updated on cancel
+ visit.cancel()
+ ms.reload()
+ self.assertTrue(ms.schedules[1].completion_status, "Pending")
+ self.assertEqual(ms.schedules[1].actual_date, None)
+
+ def test_serial_no_filters(self):
+ # Without serial no. set in schedule -> returns None
+ item_code = "_Test Serial Item"
+ make_serial_item_with_serial(item_code)
+ ms = make_maintenance_schedule(item_code=item_code)
+ ms.submit()
+
+ s_item = ms.schedules[0]
+ mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name)
+ mvi = mv.purposes[0]
+ serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name)
+ self.assertEqual(serial_nos, None)
+
+ # With serial no. set in schedule -> returns serial nos.
+ make_serial_item_with_serial(item_code)
+ ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002")
+ ms.submit()
+
+ s_item = ms.schedules[0]
+ mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name)
+ mvi = mv.purposes[0]
+ serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name)
+ self.assertEqual(serial_nos, ["TEST001", "TEST002"])
+
+ frappe.db.rollback()
+
+def make_serial_item_with_serial(item_code):
+ serial_item_doc = create_item(item_code, is_stock_item=1)
+ if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series:
+ serial_item_doc.has_serial_no = 1
+ serial_item_doc.serial_no_series = "TEST.###"
+ serial_item_doc.save(ignore_permissions=True)
+ active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code})
+ if len(active_serials) < 2:
+ make_serialized_item(item_code=item_code)
def get_events(ms):
return frappe.get_all("Event Participants", filters={
@@ -87,17 +134,18 @@ def get_events(ms):
"parenttype": "Event"
})
-def make_maintenance_schedule():
+def make_maintenance_schedule(**args):
ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company"
ms.customer = "_Test Customer"
ms.transaction_date = today()
ms.append("items", {
- "item_code": "_Test Item",
+ "item_code": args.get("item_code") or "_Test Item",
"start_date": today(),
"periodicity": "Weekly",
"no_of_visits": 4,
+ "serial_no": args.get("serial_no"),
"sales_person": "Sales Team",
})
ms.insert(ignore_permissions=True)
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index d2197a6877..72686e7403 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -2,52 +2,54 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance");
-var serial_nos = [];
frappe.ui.form.on('Maintenance Visit', {
- refresh: function (frm) {
- //filters for serial_no based on item_code
- frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) {
- let item = locals[cdt][cdn];
- if (serial_nos) {
- return {
- filters: {
- 'item_code': item.item_code,
- 'name': ["in", serial_nos]
- }
- };
- } else {
- return {
- filters: {
- 'item_code': item.item_code
- }
- };
- }
- });
- },
setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer);
},
- onload: function (frm, cdt, cdn) {
- let item = locals[cdt][cdn];
+ onload: function (frm) {
+ // filters for serial no based on item code
if (frm.doc.maintenance_type === "Scheduled") {
- const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail;
+ let item_code = frm.doc.purposes[0].item_code;
frappe.call({
- method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
+ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule",
args: {
- s_id: schedule_id
- },
- callback: function (r) {
- serial_nos = r.message;
+ schedule: frm.doc.maintenance_schedule,
+ item_code: item_code
}
+ }).then((r) => {
+ let serial_nos = r.message;
+ frm.set_query('serial_no', 'purposes', () => {
+ if (serial_nos.length > 0) {
+ return {
+ filters: {
+ 'item_code': item_code,
+ 'name': ["in", serial_nos]
+ }
+ };
+ }
+ return {
+ filters: {
+ 'item_code': item_code
+ }
+ };
+ });
+ });
+ } else {
+ frm.set_query('serial_no', 'purposes', (frm, cdt, cdn) => {
+ let row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'item_code': row.item_code
+ }
+ };
});
}
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
- frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() });
}
},
@@ -60,7 +62,6 @@ frappe.ui.form.on('Maintenance Visit', {
contact_person: function (frm) {
erpnext.utils.get_contact_details(frm);
}
-
})
// TODO commonify this code
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json
index ec32239518..4a6aa0a34b 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json
@@ -179,8 +179,7 @@
"label": "Purposes",
"oldfieldname": "maintenance_visit_details",
"oldfieldtype": "Table",
- "options": "Maintenance Visit Purpose",
- "reqd": 1
+ "options": "Maintenance Visit Purpose"
},
{
"fieldname": "more_info",
@@ -294,10 +293,11 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-05-27 16:06:17.352572",
+ "modified": "2021-12-17 03:10:27.608112",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
index 5a87b162af..6fe2466be2 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _
-from frappe.utils import get_datetime
+from frappe.utils import format_date, get_datetime
from erpnext.utilities.transaction_base import TransactionBase
@@ -18,25 +18,34 @@ class MaintenanceVisit(TransactionBase):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
frappe.throw(_("Serial No {0} does not exist").format(d.serial_no))
+ def validate_purpose_table(self):
+ if not self.purposes:
+ frappe.throw(_("Add Items in the Purpose Table"), title="Purposes Required")
+
def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference')
if item_ref:
start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
- frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date))
+ frappe.throw(_("Date must be between {0} and {1}")
+ .format(format_date(start_date), format_date(end_date)))
+
def validate(self):
self.validate_serial_no()
self.validate_maintenance_date()
+ self.validate_purpose_table()
- def update_completion_status(self):
+ def update_status_and_actual_date(self, cancel=False):
+ status = "Pending"
+ actual_date = None
+ if not cancel:
+ status = self.completion_status
+ actual_date = self.mntc_date
if self.maintenance_schedule_detail:
- frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status)
-
- def update_actual_date(self):
- if self.maintenance_schedule_detail:
- frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
+ frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status)
+ frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date)
def update_customer_issue(self, flag):
if not self.maintenance_schedule:
@@ -97,12 +106,12 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self):
self.update_customer_issue(1)
frappe.db.set(self, 'status', 'Submitted')
- self.update_completion_status()
- self.update_actual_date()
+ self.update_status_and_actual_date()
def on_cancel(self):
self.check_if_last_visit()
frappe.db.set(self, 'status', 'Cancelled')
+ self.update_status_and_actual_date(cancel=True)
def on_update(self):
pass
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 6d35d65bea..8a7634e24e 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -93,7 +93,7 @@ frappe.ui.form.on("BOM", {
});
}
- if(frm.doc.docstatus!=0) {
+ if(frm.doc.docstatus==1) {
frm.add_custom_button(__("Work Order"), function() {
frm.trigger("make_work_order");
}, __("Create"));
@@ -331,7 +331,7 @@ frappe.ui.form.on("BOM", {
});
});
- if (has_template_rm) {
+ if (has_template_rm && has_template_rm.length) {
dialog.fields_dict.items.grid.refresh();
}
},
@@ -467,7 +467,8 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
- "sourced_by_supplier": d.sourced_by_supplier
+ "sourced_by_supplier": d.sourced_by_supplier,
+ "do_not_explode": d.do_not_explode
},
callback: function(r) {
d = locals[cdt][cdn];
@@ -640,6 +641,13 @@ frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) {
});
});
+frappe.ui.form.on("BOM Item", {
+ do_not_explode: function(frm, cdt, cdn) {
+ get_bom_material_detail(frm.doc, cdt, cdn, false);
+ }
+})
+
+
frappe.ui.form.on("BOM Item", "qty", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
d.stock_qty = d.qty * d.conversion_factor;
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 218ac64d8d..0b44196940 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -37,7 +37,6 @@
"inspection_required",
"quality_inspection_template",
"column_break_31",
- "bom_level",
"section_break_33",
"items",
"scrap_section",
@@ -522,13 +521,6 @@
"fieldname": "column_break_31",
"fieldtype": "Column Break"
},
- {
- "default": "0",
- "fieldname": "bom_level",
- "fieldtype": "Int",
- "label": "BOM Level",
- "read_only": 1
- },
{
"fieldname": "section_break_33",
"fieldtype": "Section Break",
@@ -540,7 +532,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-11-18 13:04:16.271975",
+ "modified": "2022-01-30 21:27:54.727298",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
@@ -577,5 +569,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 5a60fb751d..d640f3fda7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -149,12 +149,12 @@ class BOM(WebsiteGenerator):
self.set_bom_material_details()
self.set_bom_scrap_items_detail()
self.validate_materials()
+ self.validate_transfer_against()
self.set_routing_operations()
self.validate_operations()
self.calculate_cost()
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
- self.set_bom_level()
self.validate_scrap_items()
def get_context(self, context):
@@ -203,6 +203,10 @@ class BOM(WebsiteGenerator):
for item in self.get("items"):
self.validate_bom_currency(item)
+ item.bom_no = ''
+ if not item.do_not_explode:
+ item.bom_no = item.bom_no
+
ret = self.get_bom_material_detail({
"company": self.company,
"item_code": item.item_code,
@@ -214,8 +218,10 @@ class BOM(WebsiteGenerator):
"uom": item.uom,
"stock_uom": item.stock_uom,
"conversion_factor": item.conversion_factor,
- "sourced_by_supplier": item.sourced_by_supplier
+ "sourced_by_supplier": item.sourced_by_supplier,
+ "do_not_explode": item.do_not_explode
})
+
for r in ret:
if not item.get(r):
item.set(r, ret[r])
@@ -267,6 +273,9 @@ class BOM(WebsiteGenerator):
'sourced_by_supplier' : args.get('sourced_by_supplier', 0)
}
+ if args.get('do_not_explode'):
+ ret_item['bom_no'] = ''
+
return ret_item
def validate_bom_currency(self, item):
@@ -681,6 +690,12 @@ class BOM(WebsiteGenerator):
if act_pbom and act_pbom[0][0]:
frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
+ def validate_transfer_against(self):
+ if not self.with_operations:
+ self.transfer_material_against = "Work Order"
+ if not self.transfer_material_against and not self.is_new():
+ frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+
def set_routing_operations(self):
if self.routing and self.with_operations and not self.operations:
self.get_routing()
@@ -700,20 +715,6 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
- def set_bom_level(self, update=False):
- levels = []
-
- self.bom_level = 0
- for row in self.items:
- if row.bom_no:
- levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
-
- if levels:
- self.bom_level = max(levels) + 1
-
- if update:
- self.db_set("bom_level", self.bom_level)
-
def validate_scrap_items(self):
for item in self.scrap_items:
msg = ""
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 178d92c26c..53437c8012 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -385,6 +385,53 @@ class TestBOM(ERPNextTestCase):
self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
+ def test_exclude_exploded_items_from_bom(self):
+ bom_no = get_default_bom()
+ new_bom = frappe.copy_doc(frappe.get_doc('BOM', bom_no))
+ for row in new_bom.items:
+ if row.item_code == '_Test Item Home Desktop Manufactured':
+ self.assertTrue(row.bom_no)
+ row.do_not_explode = True
+
+ new_bom.docstatus = 0
+ new_bom.save()
+ new_bom.load_from_db()
+
+ for row in new_bom.items:
+ if row.item_code == '_Test Item Home Desktop Manufactured' and row.do_not_explode:
+ self.assertFalse(row.bom_no)
+
+ new_bom.delete()
+
+ def test_valid_transfer_defaults(self):
+ bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1})
+ bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False)
+
+ # test defaults
+ bom.docstatus = 0
+ bom.transfer_material_against = None
+ bom.insert()
+ self.assertEqual(bom.transfer_material_against, "Work Order")
+
+ bom.reload()
+ bom.transfer_material_against = None
+ with self.assertRaises(frappe.ValidationError):
+ bom.save()
+ bom.reload()
+
+ # test saner default
+ bom.transfer_material_against = "Job Card"
+ bom.with_operations = 0
+ bom.save()
+ self.assertEqual(bom.transfer_material_against, "Work Order")
+
+ # test no value on existing doc
+ bom.transfer_material_against = None
+ bom.with_operations = 0
+ bom.save()
+ self.assertEqual(bom.transfer_material_against, "Work Order")
+ bom.delete()
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index 4c9877f52b..3406215cbb 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -10,6 +10,7 @@
"item_name",
"operation",
"column_break_3",
+ "do_not_explode",
"bom_no",
"source_warehouse",
"allow_alternative_item",
@@ -73,6 +74,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:!doc.do_not_explode",
"fieldname": "bom_no",
"fieldtype": "Link",
"in_filter": 1,
@@ -284,18 +286,25 @@
"fieldname": "sourced_by_supplier",
"fieldtype": "Check",
"label": "Sourced by Supplier"
+ },
+ {
+ "default": "0",
+ "fieldname": "do_not_explode",
+ "fieldtype": "Check",
+ "label": "Do Not Explode"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-08 14:19:37.563300",
+ "modified": "2022-01-24 16:57:57.020232",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 7cec7f515a..4290ca3e4c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -559,9 +559,11 @@ class ProductionPlan(Document):
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
- def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
- bom_data = sorted(bom_data, key = lambda i: i.bom_level)
+ self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
+ for idx, row in enumerate(self.sub_assembly_items, start=1):
+ row.idx = idx
+ def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
@@ -947,11 +949,8 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
locations = get_available_item_locations(item.get("item_code"),
warehouses, item.get("quantity"), company, ignore_validation=True)
- if not locations:
- new_mr_items.append(item)
- return
-
required_qty = item.get("quantity")
+ # get available material by transferring to production warehouse
for d in locations:
if required_qty <=0: return
@@ -962,14 +961,34 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
new_dict.update({
"quantity": quantity,
"material_request_type": "Material Transfer",
+ "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"from_warehouse": d.get("warehouse")
})
required_qty -= quantity
new_mr_items.append(new_dict)
+ # raise purchase request for remaining qty
if required_qty:
+ stock_uom, purchase_uom = frappe.db.get_value(
+ 'Item',
+ item['item_code'],
+ ['stock_uom', 'purchase_uom']
+ )
+
+ if purchase_uom != stock_uom and purchase_uom == item['uom']:
+ conversion_factor = get_uom_conversion_factor(item['item_code'], item['uom'])
+ if not (conversion_factor or frappe.flags.show_qty_in_stock_uom):
+ frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}")
+ .format(purchase_uom, stock_uom, item['item_code']))
+
+ required_qty = required_qty / conversion_factor
+
+ if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
+ required_qty = ceil(required_qty)
+
item["quantity"] = required_qty
+
new_mr_items.append(item)
@frappe.whitelist()
@@ -987,9 +1006,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
- bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
- if d.value else 0)
-
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
bom_data.append(frappe._dict({
'parent_item_code': parent_item_code,
@@ -1000,7 +1016,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
'uom': d.stock_uom,
'bom_no': d.value,
'is_sub_contracted_item': d.is_sub_contracted_item,
- 'bom_level': bom_level,
+ 'bom_level': indent,
'indent': indent,
'stock_qty': stock_qty
}))
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 2febc1e23c..276e70859e 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -347,6 +347,100 @@ class TestProductionPlan(ERPNextTestCase):
frappe.db.rollback()
+ def test_subassmebly_sorting(self):
+ """ Test subassembly sorting in case of multiple items with nested BOMs"""
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ prefix = "_TestLevel_"
+ boms = {
+ "Assembly": {
+ "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "SubAssembly2": {"ChildPart3": {}},
+ "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
+ "ChildPart5": {},
+ "ChildPart6": {},
+ "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
+ },
+ "MegaDeepAssy": {
+ "SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},},
+ # ^ assert that this is
+ # first item in subassy table
+ }
+ }
+ create_nested_bom(boms, prefix=prefix)
+
+ items = [prefix + item_code for item_code in boms.keys()]
+ plan = create_production_plan(item_code=items[0], do_not_save=True)
+ plan.append("po_items", {
+ 'use_multi_level_bom': 1,
+ 'item_code': items[1],
+ 'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'),
+ 'planned_qty': 1,
+ 'planned_start_date': now_datetime()
+ })
+ plan.get_sub_assembly_items()
+
+ bom_level_order = [d.bom_level for d in plan.sub_assembly_items]
+ self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True))
+ # lowest most level of subassembly should be first
+ self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
+
+ def test_multiple_work_order_for_production_plan_item(self):
+ def create_work_order(item, pln, qty):
+ # Get Production Items
+ items_data = pln.get_production_items()
+
+ # Update qty
+ items_data[(item, None, None)]["qty"] = qty
+
+ # Create and Submit Work Order for each item in items_data
+ for key, item in items_data.items():
+ if pln.sub_assembly_items:
+ item['use_multi_level_bom'] = 0
+
+ wo_name = pln.create_work_order(item)
+ wo_doc = frappe.get_doc("Work Order", wo_name)
+ wo_doc.update({
+ 'wip_warehouse': 'Work In Progress - _TC',
+ 'fg_warehouse': 'Finished Goods - _TC'
+ })
+ wo_doc.submit()
+ wo_list.append(wo_name)
+
+ item = "Test Production Item 1"
+ raw_materials = ["Raw Material Item 1", "Raw Material Item 2"]
+
+ # Create BOM
+ bom = make_bom(item=item, raw_materials=raw_materials)
+
+ # Create Production Plan
+ pln = create_production_plan(item_code=bom.item, planned_qty=10)
+
+ # All the created Work Orders
+ wo_list = []
+
+ # Create and Submit 1st Work Order for 5 qty
+ create_work_order(item, pln, 5)
+ pln.reload()
+ self.assertEqual(pln.po_items[0].ordered_qty, 5)
+
+ # Create and Submit 2nd Work Order for 3 qty
+ create_work_order(item, pln, 3)
+ pln.reload()
+ self.assertEqual(pln.po_items[0].ordered_qty, 8)
+
+ # Cancel 1st Work Order
+ wo1 = frappe.get_doc("Work Order", wo_list[0])
+ wo1.cancel()
+ pln.reload()
+ self.assertEqual(pln.po_items[0].ordered_qty, 3)
+
+ # Cancel 2nd Work Order
+ wo2 = frappe.get_doc("Work Order", wo_list[1])
+ wo2.cancel()
+ pln.reload()
+ self.assertEqual(pln.po_items[0].ordered_qty, 0)
+
def create_production_plan(**args):
args = frappe._dict(args)
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
index 657ee35a85..45ea26c3a8 100644
--- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -102,7 +102,6 @@
},
{
"columns": 1,
- "fetch_from": "bom_no.bom_level",
"fieldname": "bom_level",
"fieldtype": "Int",
"in_list_view": 1,
@@ -189,7 +188,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-28 20:10:56.296410",
+ "modified": "2022-01-30 21:31:10.527559",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
@@ -198,5 +197,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index e7eb9c6149..a399edda70 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -911,6 +911,54 @@ class TestWorkOrder(ERPNextTestCase):
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
+ def test_partial_manufacture_entries(self):
+ cancel_stock_entry = []
+
+ frappe.db.set_value("Manufacturing Settings", None,
+ "backflush_raw_materials_based_on", "Material Transferred for Manufacture")
+
+ wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100)
+ ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
+ target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0)
+ ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0)
+
+ cancel_stock_entry.extend([ste1.name, ste2.name])
+
+ sm = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 100))
+ for row in sm.get('items'):
+ if row.get('item_code') == '_Test Item':
+ row.qty = 110
+
+ sm.submit()
+ cancel_stock_entry.append(sm.name)
+
+ s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 90))
+ for row in s.get('items'):
+ if row.get('item_code') == '_Test Item':
+ self.assertEqual(row.get('qty'), 100)
+ s.submit()
+ cancel_stock_entry.append(s.name)
+
+ s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
+ for row in s1.get('items'):
+ if row.get('item_code') == '_Test Item':
+ self.assertEqual(row.get('qty'), 5)
+ s1.submit()
+ cancel_stock_entry.append(s1.name)
+
+ s2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
+ for row in s2.get('items'):
+ if row.get('item_code') == '_Test Item':
+ self.assertEqual(row.get('qty'), 5)
+
+ cancel_stock_entry.reverse()
+ for ste in cancel_stock_entry:
+ doc = frappe.get_doc("Stock Entry", ste)
+ doc.cancel()
+
+ frappe.db.set_value("Manufacturing Settings", None,
+ "backflush_raw_materials_based_on", "BOM")
def update_job_card(job_card, jc_qty=None):
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 5ffbb0374e..6433a99283 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -131,16 +131,14 @@ frappe.ui.form.on("Work Order", {
erpnext.work_order.set_custom_buttons(frm);
frm.set_intro("");
- if (frm.doc.docstatus === 0 && !frm.doc.__islocal) {
+ if (frm.doc.docstatus === 0 && !frm.is_new()) {
frm.set_intro(__("Submit this Work Order for further processing."));
+ } else {
+ frm.trigger("show_progress_for_items");
+ frm.trigger("show_progress_for_operations");
}
if (frm.doc.status != "Closed") {
- if (frm.doc.docstatus===1) {
- frm.trigger('show_progress_for_items');
- frm.trigger('show_progress_for_operations');
- }
-
if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length) {
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 12cd58f418..9452a63d70 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -333,12 +333,13 @@
"options": "fa fa-wrench"
},
{
- "default": "Work Order",
"depends_on": "operations",
+ "fetch_from": "bom_no.transfer_material_against",
+ "fetch_if_empty": 1,
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
- "options": "Work Order\nJob Card"
+ "options": "\nWork Order\nJob Card"
},
{
"fieldname": "operations",
@@ -574,7 +575,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-11-08 17:36:07.016300",
+ "modified": "2022-01-24 21:18:12.160114",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -607,6 +608,7 @@
],
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"title_field": "production_item",
"track_changes": 1,
"track_seen": 1
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 170454c823..a86edfa45f 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -31,6 +31,7 @@ from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import (
auto_make_serial_nos,
+ clean_serial_no_string,
get_auto_serial_nos,
get_serial_nos,
)
@@ -65,6 +66,7 @@ class WorkOrder(Document):
self.validate_warehouse_belongs_to_company()
self.calculate_operating_cost()
self.validate_qty()
+ self.validate_transfer_against()
self.validate_operation_time()
self.status = self.get_status()
@@ -72,6 +74,7 @@ class WorkOrder(Document):
self.set_required_items(reset_only_qty = len(self.get("required_items")))
+
def validate_sales_order(self):
if self.sales_order:
self.check_sales_order_on_hold_or_close()
@@ -356,6 +359,7 @@ class WorkOrder(Document):
frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args):
+ self.serial_no = clean_serial_no_string(self.serial_no)
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
@@ -445,7 +449,13 @@ class WorkOrder(Document):
def update_ordered_qty(self):
if self.production_plan and self.production_plan_item:
- qty = self.qty if self.docstatus == 1 else 0
+ qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
+
+ if self.docstatus == 1:
+ qty += self.qty
+ elif self.docstatus == 2:
+ qty -= self.qty
+
frappe.db.set_value('Production Plan Item',
self.production_plan_item, 'ordered_qty', qty)
@@ -625,6 +635,16 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
+ def validate_transfer_against(self):
+ if not self.docstatus == 1:
+ # let user configure operations until they're ready to submit
+ return
+ if not self.operations:
+ self.transfer_material_against = "Work Order"
+ if not self.transfer_material_against:
+ frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+
+
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index 25de2e0379..19a80ab407 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
'item_code': item.item_code,
'item_name': item.item_name,
'indent': indent,
- 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
- if item.bom_no else ""),
+ 'bom_level': indent,
'bom': item.bom_no,
'qty': item.qty * qty,
'uom': item.uom,
@@ -73,7 +72,7 @@ def get_columns():
},
{
"label": "BOM Level",
- "fieldtype": "Data",
+ "fieldtype": "Int",
"fieldname": "bom_level",
"width": 100
},
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 7468e34020..0eb22a22f7 100644
--- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
+++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js
@@ -4,6 +4,39 @@
frappe.query_reports["BOM Operations Time"] = {
"filters": [
-
+ {
+ "fieldname": "item_code",
+ "label": __("Item Code"),
+ "fieldtype": "Link",
+ "width": "100",
+ "options": "Item",
+ "get_query": () =>{
+ return {
+ filters: { "disabled": 0, "is_stock_item": 1 }
+ }
+ }
+ },
+ {
+ "fieldname": "bom_id",
+ "label": __("BOM ID"),
+ "fieldtype": "MultiSelectList",
+ "width": "100",
+ "options": "BOM",
+ "get_data": function(txt) {
+ return frappe.db.get_link_options("BOM", txt);
+ },
+ "get_query": () =>{
+ return {
+ filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 }
+ }
+ }
+ },
+ {
+ "fieldname": "workstation",
+ "label": __("Workstation"),
+ "fieldtype": "Link",
+ "width": "100",
+ "options": "Workstation"
+ },
]
};
diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json
index 665c5b9f79..8162017ca8 100644
--- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json
+++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json
@@ -1,14 +1,16 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
+ "columns": [],
"creation": "2020-03-03 01:41:20.862521",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "",
- "modified": "2020-03-03 01:41:20.862521",
+ "modified": "2022-01-20 14:21:47.771591",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operations Time",
diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py
index e7a818abd5..eda9eb9d70 100644
--- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py
+++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py
@@ -12,19 +12,15 @@ def execute(filters=None):
return columns, data
def get_data(filters):
- data = []
+ bom_wise_data = {}
+ bom_data, report_data = [], []
- bom_data = []
- for d in frappe.db.sql("""
- SELECT
- bom.name, bom.item, bom.item_name, bom.uom,
- bomps.operation, bomps.workstation, bomps.time_in_mins
- FROM `tabBOM` bom, `tabBOM Operation` bomps
- WHERE
- bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent
- """, as_dict=1):
+ bom_operation_data = get_filtered_data(filters)
+
+ for d in bom_operation_data:
row = get_args()
if d.name not in bom_data:
+ bom_wise_data[d.name] = []
bom_data.append(d.name)
row.update(d)
else:
@@ -34,14 +30,49 @@ def get_data(filters):
"time_in_mins": d.time_in_mins
})
- data.append(row)
+ # maintain BOM wise data for grouping such as:
+ # {"BOM A": [{Row1}, {Row2}], "BOM B": ...}
+ bom_wise_data[d.name].append(row)
used_as_subassembly_items = get_bom_count(bom_data)
- for d in data:
- d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0)
+ for d in bom_wise_data:
+ for row in bom_wise_data[d]:
+ row.used_as_subassembly_items = used_as_subassembly_items.get(row.name, 0)
+ report_data.append(row)
- return data
+ return report_data
+
+def get_filtered_data(filters):
+ bom = frappe.qb.DocType("BOM")
+ bom_ops = frappe.qb.DocType("BOM Operation")
+
+ bom_ops_query = (
+ frappe.qb.from_(bom)
+ .join(bom_ops).on(bom.name == bom_ops.parent)
+ .select(
+ bom.name, bom.item, bom.item_name, bom.uom,
+ bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins
+ ).where(
+ (bom.docstatus == 1)
+ & (bom.is_active == 1)
+ )
+ )
+
+ if filters.get("item_code"):
+ bom_ops_query = bom_ops_query.where(bom.item == filters.get("item_code"))
+
+ if filters.get("bom_id"):
+ bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id")))
+
+ if filters.get("workstation"):
+ bom_ops_query = bom_ops_query.where(
+ bom_ops.workstation == filters.get("workstation")
+ )
+
+ bom_operation_data = bom_ops_query.run(as_dict=True)
+
+ return bom_operation_data
def get_bom_count(bom_data):
data = frappe.get_all("BOM Item",
@@ -68,13 +99,13 @@ def get_columns(filters):
"options": "BOM",
"fieldname": "name",
"fieldtype": "Link",
- "width": 140
+ "width": 220
}, {
- "label": _("BOM Item Code"),
+ "label": _("Item Code"),
"options": "Item",
"fieldname": "item",
"fieldtype": "Link",
- "width": 140
+ "width": 150
}, {
"label": _("Item Name"),
"fieldname": "item_name",
@@ -85,13 +116,13 @@ def get_columns(filters):
"options": "UOM",
"fieldname": "uom",
"fieldtype": "Link",
- "width": 140
+ "width": 100
}, {
"label": _("Operation"),
"options": "Operation",
"fieldname": "operation",
"fieldtype": "Link",
- "width": 120
+ "width": 140
}, {
"label": _("Workstation"),
"options": "Workstation",
@@ -101,11 +132,11 @@ def get_columns(filters):
}, {
"label": _("Time (In Mins)"),
"fieldname": "time_in_mins",
- "fieldtype": "Int",
- "width": 140
+ "fieldtype": "Float",
+ "width": 120
}, {
"label": _("Sub-assembly BOM Count"),
"fieldname": "used_as_subassembly_items",
"fieldtype": "Int",
- "width": 180
+ "width": 200
}]
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
index 97e7e0a7d2..72eed5e0d7 100644
--- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -17,14 +17,12 @@ frappe.query_reports["Cost of Poor Quality Report"] = {
fieldname:"from_date",
fieldtype: "Datetime",
default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
- reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Datetime",
default: frappe.datetime.now_datetime(),
- reqd: 1,
},
{
label: __("Job Card"),
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
index 77418235b0..88b21170e8 100644
--- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -3,46 +3,65 @@
import frappe
from frappe import _
-from frappe.utils import flt
def execute(filters=None):
- columns, data = [], []
+ return get_columns(filters), get_data(filters)
- columns = get_columns(filters)
- data = get_data(filters)
-
- return columns, data
def get_data(report_filters):
data = []
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
if operations:
- operations = [d.name for d in operations]
- fields = ["production_item as item_code", "item_name", "work_order", "operation",
- "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
+ if report_filters.get('operation'):
+ operations = [report_filters.get('operation')]
+ else:
+ operations = [d.name for d in operations]
- filters = get_filters(report_filters, operations)
+ job_card = frappe.qb.DocType("Job Card")
- job_cards = frappe.get_all("Job Card", fields = fields,
- filters = filters)
+ operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost')
+ item_code = (job_card.production_item).as_('item_code')
- for row in job_cards:
- row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
- data.append(row)
+ query = (frappe.qb
+ .from_(job_card)
+ .select(job_card.name, job_card.work_order, item_code, job_card.item_name,
+ job_card.operation, job_card.serial_no, job_card.batch_no,
+ job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate,
+ operating_cost)
+ .where(
+ (job_card.docstatus == 1)
+ & (job_card.is_corrective_job_card == 1))
+ .groupby(job_card.name)
+ )
+ query = append_filters(query, report_filters, operations, job_card)
+ data = query.run(as_dict=True)
return data
-def get_filters(report_filters, operations):
- filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
- for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
- if report_filters.get(field):
- if field != 'serial_no':
- filters[field] = report_filters.get(field)
- else:
- filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
+def append_filters(query, report_filters, operations, job_card):
+ """Append optional filters to query builder. """
- return filters
+ for field in ("name", "work_order", "operation", "workstation",
+ "company", "serial_no", "batch_no", "production_item"):
+ if report_filters.get(field):
+ if field == 'serial_no':
+ query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field))))
+ elif field == 'operation':
+ query = query.where(job_card[field].isin(operations))
+ else:
+ query = query.where(job_card[field] == report_filters.get(field))
+
+ if report_filters.get('from_date') or report_filters.get('to_date'):
+ job_card_time_log = frappe.qb.DocType("Job Card Time Log")
+
+ query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent)
+ if report_filters.get('from_date'):
+ query = query.where(job_card_time_log.from_time >= report_filters.get('from_date'))
+ if report_filters.get('to_date'):
+ query = query.where(job_card_time_log.to_time <= report_filters.get('to_date'))
+
+ return query
def get_columns(filters):
return [
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
index 55b1a3f2f9..aaa231466f 100644
--- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
@@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details):
"qty": row.planned_qty,
"document_type": "Work Order",
"document_name": work_order or "",
- "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
+ "bom_level": 0,
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
})
diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
index 8368db6374..e1e7225e05 100644
--- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
+++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
@@ -172,10 +172,15 @@ class ProductionPlanReport(object):
self.purchase_details = {}
- for d in frappe.get_all("Purchase Order Item",
+ purchased_items = frappe.get_all("Purchase Order Item",
fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"],
- filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)},
- group_by = "item_code, warehouse"):
+ filters={
+ "item_code": ("in", self.item_codes),
+ "warehouse": ("in", self.warehouses),
+ "docstatus": 1,
+ },
+ group_by = "item_code, warehouse")
+ for d in purchased_items:
key = (d.item_code, d.warehouse)
if key not in self.purchase_details:
self.purchase_details.setdefault(key, d)
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 1de472659e..9f51ded6c7 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -18,7 +18,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("BOM Operations Time", {}),
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
- ("Cost of Poor Quality Report", {}),
+ ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
("Downtime Analysis", {}),
(
"Exponential Smoothing Forecasting",
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 65b4d02639..05ca2a8452 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"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\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"docstatus": 0,
"doctype": "Workspace",
@@ -402,7 +402,7 @@
"type": "Link"
}
],
- "modified": "2021-11-22 17:55:03.524496",
+ "modified": "2022-01-13 17:40:09.474747",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -411,7 +411,7 @@
"public": 1,
"restrict_to_domain": "Manufacturing",
"roles": [],
- "sequence_id": 17,
+ "sequence_id": 17.0,
"shortcuts": [
{
"color": "Green",
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index ae0bb2d5c9..c5705c1763 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -9,18 +9,16 @@ Manufacturing
Stock
Support
Utilities
-Shopping Cart
Assets
Portal
Maintenance
Education
Regional
-Restaurant
ERPNext Integrations
Non Profit
-Hotels
Quality Management
Communication
Loan Management
Payroll
-Telephony
\ No newline at end of file
+Telephony
+E-commerce
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
index ba2f919d01..fc90475fb3 100644
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ b/erpnext/non_profit/workspace/non_profit/non_profit.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Member\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Profit Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Membership\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter Member\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Grant Application\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Membership\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Volunteer\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Donation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tax Exemption Certification (India)\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Member\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Profit Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Membership\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chapter Member\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loan Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Grant Application\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Membership\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Volunteer\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Chapter\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Donation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tax Exemption Certification (India)\",\"col\":4}}]",
"creation": "2020-03-02 17:23:47.811421",
"docstatus": 0,
"doctype": "Workspace",
@@ -231,7 +231,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.146207",
+ "modified": "2022-01-13 17:40:50.220877",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
@@ -240,7 +240,7 @@
"public": 1,
"restrict_to_domain": "Non Profit",
"roles": [],
- "sequence_id": 18,
+ "sequence_id": 18.0,
"shortcuts": [
{
"label": "Member",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 1683dabccc..feafecbc04 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -1,4 +1,6 @@
+[pre_model_sync]
erpnext.patches.v12_0.update_is_cancelled_field
+erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.rename_production_order_to_work_order
erpnext.patches.v11_0.refactor_naming_series
erpnext.patches.v11_0.refactor_autoname_naming
@@ -202,6 +204,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
erpnext.patches.v12_0.create_itc_reversal_custom_fields
+execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings")
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
erpnext.patches.v12_0.add_taxjar_integration_field
@@ -222,11 +225,11 @@ erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.set_app_name
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
+erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
erpnext.patches.v13_0.update_member_email_address
-erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
erpnext.patches.v13_0.add_po_to_global_search
@@ -242,6 +245,8 @@ erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition
+erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
+erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
@@ -252,9 +257,10 @@ erpnext.patches.v12_0.add_gst_category_in_delivery_note
erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
-erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
+erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022
erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
+erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
@@ -266,12 +272,11 @@ erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.update_response_by_variance
-erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
-erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
+erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v13_0.delete_orphaned_tables
erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
erpnext.patches.v13_0.update_tds_check_field #3
@@ -279,18 +284,18 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.remove_bad_selling_defaults
-erpnext.patches.v13_0.trim_whitespace_from_serial_nos
+erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
-erpnext.patches.v13_0.einvoicing_deprecation_warning
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
-erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
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.v14_0.delete_shopify_doctypes
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
@@ -312,17 +317,36 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2
-erpnext.patches.v14_0.delete_hub_doctypes
-erpnext.patches.v13_0.create_ksa_vat_custom_fields
-erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
+erpnext.patches.v13_0.fetch_thumbnail_in_website_items
+erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
+erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field
+erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
-erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
erpnext.patches.v13_0.update_tax_category_for_rcm
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
-erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.update_exchange_rate_settings
+erpnext.patches.v13_0.update_asset_quantity_field
+erpnext.patches.v13_0.delete_bank_reconciliation_detail
+erpnext.patches.v13_0.enable_provisional_accounting
+
+[post_model_sync]
+erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
+erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
+erpnext.patches.v14_0.delete_shopify_doctypes
+erpnext.patches.v14_0.delete_hub_doctypes
+erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
+erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v14_0.rearrange_company_fields
+erpnext.patches.v14_0.update_leave_notification_template
+erpnext.patches.v14_0.restore_einvoice_fields
+erpnext.patches.v13_0.update_sane_transfer_against
+erpnext.patches.v12_0.add_company_link_to_einvoice_settings
+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_disbursement_account
diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
new file mode 100644
index 0000000000..e498b673fb
--- /dev/null
+++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
@@ -0,0 +1,18 @@
+from __future__ import unicode_literals
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company or not frappe.db.count('E Invoice User'):
+ return
+
+ frappe.reload_doc("regional", "doctype", "e_invoice_user")
+ for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
+ company_name = frappe.db.sql("""
+ select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
+ where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
+ """, (creds.get('gstin')))
+ if company_name and len(company_name) > 0:
+ frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])
diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py
new file mode 100644
index 0000000000..aeff9ca841
--- /dev/null
+++ b/erpnext/patches/v12_0/add_einvoice_status_field.py
@@ -0,0 +1,72 @@
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ # move hidden einvoice fields to a different section
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
+ print_hide=1, hidden=1),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
+ options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
+ hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
+
+ if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'):
+ frappe.db.sql('''
+ UPDATE `tabSales Invoice` SET einvoice_status = 'Pending'
+ WHERE
+ posting_date >= '2021-04-01'
+ AND ifnull(irn, '') = ''
+ AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '')
+ AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export')
+ ''')
+
+ # set appropriate statuses
+ frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated'
+ WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''')
+
+ frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled'
+ WHERE ifnull(irn_cancelled, 0) = 1''')
+
+ # set correct acknowledgement in e-invoices
+ einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice'])
+
+ if einvoices:
+ for inv in einvoices:
+ signed_einvoice = inv.get('signed_einvoice')
+ if signed_einvoice:
+ signed_einvoice = json.loads(signed_einvoice)
+ frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False)
+ frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False)
diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
new file mode 100644
index 0000000000..e837786138
--- /dev/null
+++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
@@ -0,0 +1,20 @@
+from __future__ import unicode_literals
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ if frappe.db.exists('Report', 'E-Invoice Summary') and \
+ not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')):
+ frappe.get_doc(dict(
+ doctype='Custom Role',
+ report='E-Invoice Summary',
+ roles= [
+ dict(role='Accounts User'),
+ dict(role='Accounts Manager')
+ ]
+ )).insert()
diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py
new file mode 100644
index 0000000000..247140d21d
--- /dev/null
+++ b/erpnext/patches/v12_0/add_ewaybill_validity_field.py
@@ -0,0 +1,18 @@
+from __future__ import unicode_literals
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1,
+ depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill')
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py
new file mode 100644
index 0000000000..c17666add1
--- /dev/null
+++ b/erpnext/patches/v12_0/setup_einvoice_fields.py
@@ -0,0 +1,59 @@
+from __future__ import unicode_literals
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+from erpnext.regional.india.setup import add_permissions, add_print_formats
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ frappe.reload_doc("custom", "doctype", "custom_field")
+ frappe.reload_doc("regional", "doctype", "e_invoice_settings")
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
+ depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
+ add_permissions()
+ add_print_formats()
+
+ einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
+ t = {
+ 'mode_of_transport': [{'default': None}],
+ 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
+ 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
+ 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
+ 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
+ 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
+ 'ewaybill': [
+ {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
+ {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
+ ]
+ }
+
+ for field, conditions in t.items():
+ for c in conditions:
+ [(prop, value)] = c.items()
+ frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)
diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
new file mode 100644
index 0000000000..3f90a03020
--- /dev/null
+++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'})
+ if irn_cancelled_field:
+ frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn')
+ frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0)
diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py
index df7875079b..0401034874 100644
--- a/erpnext/patches/v12_0/update_is_cancelled_field.py
+++ b/erpnext/patches/v12_0/update_is_cancelled_field.py
@@ -2,14 +2,28 @@ import frappe
def execute():
- try:
- frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')")
- frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')")
+ #handle type casting for is_cancelled field
+ module_doctypes = (
+ ('stock', 'Stock Ledger Entry'),
+ ('stock', 'Serial No'),
+ ('accounts', 'GL Entry')
+ )
- frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 1 where is_cancelled = 'Yes'")
- frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 1 where is_cancelled = 'Yes'")
+ for module, doctype in module_doctypes:
+ if (not frappe.db.has_column(doctype, "is_cancelled")
+ or frappe.db.get_column_type(doctype, "is_cancelled").lower() == "int(1)"
+ ):
+ continue
- frappe.reload_doc("stock", "doctype", "stock_ledger_entry")
- frappe.reload_doc("stock", "doctype", "serial_no")
- except Exception:
- pass
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET is_cancelled = 0
+ where is_cancelled in ('', NULL, 'No')"""
+ .format(doctype=doctype))
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET is_cancelled = 1
+ where is_cancelled = 'Yes'"""
+ .format(doctype=doctype))
+
+ frappe.reload_doc(module, "doctype", frappe.scrub(doctype))
diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py
new file mode 100644
index 0000000000..57fbaae9d8
--- /dev/null
+++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py
@@ -0,0 +1,63 @@
+import frappe
+
+from erpnext.stock.stock_balance import (
+ get_balance_qty_from_sle,
+ get_indented_qty,
+ get_ordered_qty,
+ get_planned_qty,
+ get_reserved_qty,
+)
+from erpnext.stock.utils import get_bin
+
+
+def execute():
+ delete_broken_bins()
+ delete_and_patch_duplicate_bins()
+
+def delete_broken_bins():
+ # delete useless bins
+ frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null")
+
+def delete_and_patch_duplicate_bins():
+
+ duplicate_bins = frappe.db.sql("""
+ SELECT
+ item_code, warehouse, count(*) as bin_count
+ FROM
+ tabBin
+ GROUP BY
+ item_code, warehouse
+ HAVING
+ bin_count > 1
+ """, as_dict=1)
+
+ for duplicate_bin in duplicate_bins:
+ item_code = duplicate_bin.item_code
+ warehouse = duplicate_bin.warehouse
+ existing_bins = frappe.get_list("Bin",
+ filters={
+ "item_code": item_code,
+ "warehouse": warehouse
+ },
+ fields=["name"],
+ order_by="creation",)
+
+ # keep last one
+ existing_bins.pop()
+
+ for broken_bin in existing_bins:
+ frappe.delete_doc("Bin", broken_bin.name)
+
+ qty_dict = {
+ "reserved_qty": get_reserved_qty(item_code, warehouse),
+ "indented_qty": get_indented_qty(item_code, warehouse),
+ "ordered_qty": get_ordered_qty(item_code, warehouse),
+ "planned_qty": get_planned_qty(item_code, warehouse),
+ "actual_qty": get_balance_qty_from_sle(item_code, warehouse)
+ }
+
+ bin = get_bin(item_code, warehouse)
+ bin.update(qty_dict)
+ bin.update_reserved_qty_for_production()
+ bin.update_reserved_qty_for_sub_contracting()
+ bin.db_update()
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
new file mode 100644
index 0000000000..d3ee3f8276
--- /dev/null
+++ b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py
@@ -0,0 +1,57 @@
+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:
+ title = f"{item}: Error while converting to Website Item "
+ frappe.log_error(title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title)
+ return None
diff --git a/erpnext/patches/v13_0/create_website_items.py b/erpnext/patches/v13_0/create_website_items.py
new file mode 100644
index 0000000000..da162a3ab1
--- /dev/null
+++ b/erpnext/patches/v13_0/create_website_items.py
@@ -0,0 +1,72 @@
+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", "image",
+ "has_variants", "variant_of", "description", "weightage"]
+ web_fields_to_map = ["route", "slideshow", "website_image_alt",
+ "website_warehouse", "web_long_description", "website_content", "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/delete_bank_reconciliation_detail.py b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py
new file mode 100644
index 0000000000..75953b0e30
--- /dev/null
+++ b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+import frappe
+
+
+def execute():
+
+ if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \
+ frappe.db.exists('DocType', 'Bank Clearance Detail'):
+
+ frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1)
diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py
index c597fe8645..e6eba0a608 100644
--- a/erpnext/patches/v13_0/delete_old_sales_reports.py
+++ b/erpnext/patches/v13_0/delete_old_sales_reports.py
@@ -12,6 +12,7 @@ def execute():
for report in reports_to_delete:
if frappe.db.exists("Report", report):
+ delete_links_from_desktop_icons(report)
delete_auto_email_reports(report)
check_and_delete_linked_reports(report)
@@ -22,3 +23,9 @@ def delete_auto_email_reports(report):
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0])
+
+def delete_links_from_desktop_icons(report):
+ """ Check for one or multiple Desktop Icons and delete """
+ desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
+ for desktop_icon in desktop_icons:
+ frappe.delete_doc("Desktop Icon", desktop_icon[0])
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/einvoicing_deprecation_warning.py b/erpnext/patches/v13_0/einvoicing_deprecation_warning.py
deleted file mode 100644
index e123a55f5a..0000000000
--- a/erpnext/patches/v13_0/einvoicing_deprecation_warning.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import click
-
-
-def execute():
- click.secho(
- "Indian E-Invoicing integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
- "Please install the app to continue using the integration: https://github.com/frappe/erpnext_gst_compliance",
- fg="yellow",
- )
diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py
new file mode 100644
index 0000000000..8e222700f8
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_provisional_accounting.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("setup", "doctype", "company")
+
+ company = frappe.qb.DocType("Company")
+
+ frappe.qb.update(
+ company
+ ).set(
+ company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items
+ ).set(
+ company.default_provisional_account, company.service_received_but_not_billed
+ ).where(
+ company.enable_perpetual_inventory_for_non_stock_items == 1
+ ).where(
+ company.service_received_but_not_billed.isnotnull()
+ ).run()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
new file mode 100644
index 0000000000..32ad542cf8
--- /dev/null
+++ b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
@@ -0,0 +1,16 @@
+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/hospitality_deprecation_warning.py b/erpnext/patches/v13_0/hospitality_deprecation_warning.py
new file mode 100644
index 0000000000..2708b2ccd3
--- /dev/null
+++ b/erpnext/patches/v13_0/hospitality_deprecation_warning.py
@@ -0,0 +1,10 @@
+import click
+
+
+def execute():
+
+ click.secho(
+ "Hospitality domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
+ "When upgrading to ERPNext version-14, please install the app to continue using the Hospitality domain: https://github.com/frappe/hospitality",
+ fg="yellow",
+ )
diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py
new file mode 100644
index 0000000000..7a7ddba12d
--- /dev/null
+++ b/erpnext/patches/v13_0/make_homepage_products_website_items.py
@@ -0,0 +1,15 @@
+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()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/make_non_standard_user_type.py b/erpnext/patches/v13_0/make_non_standard_user_type.py
index a7bdf93332..ff241a3fd9 100644
--- a/erpnext/patches/v13_0/make_non_standard_user_type.py
+++ b/erpnext/patches/v13_0/make_non_standard_user_type.py
@@ -10,8 +10,15 @@ from erpnext.setup.install import add_non_standard_user_types
def execute():
doctype_dict = {
'projects': ['Timesheet'],
- 'payroll': ['Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission'],
- 'hr': ['Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request']
+ 'payroll': [
+ 'Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission',
+ 'Employee Benefit Application', 'Employee Benefit Claim'
+ ],
+ 'hr': [
+ 'Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request',
+ 'Holiday List', 'Employee Advance', 'Training Program', 'Training Feedback',
+ 'Shift Request', 'Employee Grievance', 'Employee Referral', 'Travel Request'
+ ]
}
for module, doctypes in doctype_dict.items():
diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py
new file mode 100644
index 0000000000..8f9ee512fd
--- /dev/null
+++ b/erpnext/patches/v13_0/populate_e_commerce_settings.py
@@ -0,0 +1,62 @@
+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/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
index 5487a6c60c..02625396dd 100644
--- a/erpnext/patches/v13_0/remove_bad_selling_defaults.py
+++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
@@ -3,6 +3,7 @@ from frappe import _
def execute():
+ frappe.reload_doctype('Selling Settings')
selling_settings = frappe.get_single("Selling Settings")
if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
index 7a2a253967..2d35ea3458 100644
--- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
+++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
@@ -5,6 +5,9 @@ from erpnext.regional.india.setup import make_custom_fields
def execute():
if frappe.get_all('Company', filters = {'country': 'India'}):
+ frappe.reload_doc('accounts', 'doctype', 'POS Invoice')
+ frappe.reload_doc('accounts', 'doctype', 'POS Invoice Item')
+
make_custom_fields()
if not frappe.db.exists('Party Type', 'Donor'):
diff --git a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
new file mode 100644
index 0000000000..35710a9bb4
--- /dev/null
+++ b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
@@ -0,0 +1,29 @@
+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/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
index 8a9633d896..4ec22e9d0e 100644
--- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
+++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
@@ -9,13 +9,15 @@ def execute():
from `tabStock Ledger Entry`
where
is_cancelled = 0
- and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s)
+ and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s
+ or serial_no = %s )
""",
(
" %", # leading whitespace
"% ", # trailing whitespace
"%\n %", # leading whitespace on newline
"% \n%", # trailing whitespace on newline
+ "\n", # just new line
),
as_dict=True,
)
diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
index 55fd465b20..60466eb86a 100644
--- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
+++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py
@@ -37,4 +37,4 @@ def execute():
jc.production_item = wo.production_item, jc.item_name = wo.item_name
WHERE
jc.work_order = wo.name and IFNULL(jc.production_item, "") = ""
- """)
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_asset_quantity_field.py b/erpnext/patches/v13_0/update_asset_quantity_field.py
new file mode 100644
index 0000000000..47884d1968
--- /dev/null
+++ b/erpnext/patches/v13_0/update_asset_quantity_field.py
@@ -0,0 +1,8 @@
+import frappe
+
+
+def execute():
+ if frappe.db.count('Asset'):
+ frappe.reload_doc("assets", "doctype", "Asset")
+ asset = frappe.qb.DocType('Asset')
+ frappe.qb.update(asset).set(asset.asset_quantity, 1).run()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py
deleted file mode 100644
index 499412ee27..0000000000
--- a/erpnext/patches/v13_0/update_level_in_bom.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (c) 2020, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def execute():
- for document in ["bom", "bom_item", "bom_explosion_item"]:
- frappe.reload_doc('manufacturing', 'doctype', document)
-
- frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
-
- bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
- where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
- where parent=bom.name and ifnull(bom_no, '')!='')""")
-
- count = 0
- while(count < len(bom_list)):
- for parent_bom in get_parent_boms(bom_list[count]):
- bom_doc = frappe.get_cached_doc("BOM", parent_bom)
- bom_doc.set_bom_level(update=True)
- bom_list.append(parent_bom)
- count += 1
-
-def get_parent_boms(bom_no):
- return frappe.db.sql_list("""
- select distinct bom_item.parent from `tabBOM Item` bom_item
- where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
- and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
- """, bom_no)
diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py
new file mode 100644
index 0000000000..7a8c1c6135
--- /dev/null
+++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py
@@ -0,0 +1,25 @@
+
+import frappe
+
+
+def execute():
+ frappe.reload_doctype('Maintenance Visit')
+ frappe.reload_doctype('Maintenance Visit Purpose')
+
+ # Updates the Maintenance Schedule link to fetch serial nos
+ from frappe.query_builder.functions import Coalesce
+ mvp = frappe.qb.DocType('Maintenance Visit Purpose')
+ mv = frappe.qb.DocType('Maintenance Visit')
+
+ frappe.qb.update(
+ mv
+ ).join(
+ mvp
+ ).on(mvp.parent == mv.name).set(
+ mv.maintenance_schedule,
+ Coalesce(mvp.prevdoc_docname, '')
+ ).where(
+ (mv.maintenance_type == "Scheduled")
+ & (mvp.prevdoc_docname.notnull())
+ & (mv.docstatus < 2)
+ ).run(as_dict=1)
diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py
new file mode 100644
index 0000000000..a163d38584
--- /dev/null
+++ b/erpnext/patches/v13_0/update_sane_transfer_against.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+ bom = frappe.qb.DocType("BOM")
+
+ (frappe.qb
+ .update(bom)
+ .set(bom.transfer_material_against, "Work Order")
+ .where(bom.with_operations == 0)
+ ).run()
diff --git a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py
new file mode 100644
index 0000000000..e43a8bad8e
--- /dev/null
+++ b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py
@@ -0,0 +1,18 @@
+import frappe
+
+
+def execute():
+
+ doctype = "Stock Reconciliation Item"
+
+ if not frappe.db.has_column(doctype, "current_serial_no"):
+ # nothing to fix if column doesn't exist
+ return
+
+ sr_item = frappe.qb.DocType(doctype)
+
+ (frappe.qb
+ .update(sr_item)
+ .set(sr_item.current_serial_no, None)
+ .where(sr_item.current_qty == 0)
+ ).run()
diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
index 120182a80e..2a8b6ef746 100644
--- a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
+++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
@@ -5,9 +5,6 @@ from frappe import _
def execute():
- frappe.reload_doc("email", "doctype", "email_template")
- frappe.reload_doc("hr", "doctype", "hr_settings")
-
template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
if not template:
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
diff --git a/erpnext/patches/v14_0/delete_einvoicing_doctypes.py b/erpnext/patches/v14_0/delete_einvoicing_doctypes.py
deleted file mode 100644
index a3a8149be3..0000000000
--- a/erpnext/patches/v14_0/delete_einvoicing_doctypes.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import frappe
-
-
-def execute():
- frappe.delete_doc('DocType', 'E Invoice Settings', ignore_missing=True)
- frappe.delete_doc('DocType', 'E Invoice User', ignore_missing=True)
- frappe.delete_doc('Report', 'E-Invoice Summary', ignore_missing=True)
- frappe.delete_doc('Print Format', 'GST E-Invoice', ignore_missing=True)
- frappe.delete_doc('Custom Field', 'Sales Invoice-eway_bill_cancelled', ignore_missing=True)
- frappe.delete_doc('Custom Field', 'Sales Invoice-irn_cancelled', ignore_missing=True)
diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
index 28fc01beab..3a4f8f537d 100644
--- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py
+++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
@@ -47,3 +47,18 @@ def execute():
frappe.delete_doc("DocType", doctype, ignore_missing=True)
frappe.delete_doc("Module Def", "Healthcare", ignore_missing=True, force=True)
+
+ custom_fields = {
+ 'Sales Invoice': ['patient', 'patient_name', 'ref_practitioner'],
+ 'Sales Invoice Item': ['reference_dt', 'reference_dn'],
+ 'Stock Entry': ['inpatient_medication_entry'],
+ 'Stock Entry Detail': ['patient', 'inpatient_medication_entry_child'],
+ }
+ for doc, fields in custom_fields.items():
+ filters = {
+ 'dt': doc,
+ 'fieldname': ['in', fields]
+ }
+ records = frappe.get_all('Custom Field', filters=filters, pluck='name')
+ for record in records:
+ frappe.delete_doc('Custom Field', record, ignore_missing=True, force=True)
diff --git a/erpnext/patches/v14_0/delete_hospitality_doctypes.py b/erpnext/patches/v14_0/delete_hospitality_doctypes.py
new file mode 100644
index 0000000000..d0216f80d3
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_hospitality_doctypes.py
@@ -0,0 +1,32 @@
+import frappe
+
+
+def execute():
+ modules = ['Hotels', 'Restaurant']
+
+ for module in modules:
+ frappe.delete_doc("Module Def", module, ignore_missing=True, force=True)
+
+ frappe.delete_doc("Workspace", module, ignore_missing=True, force=True)
+
+ reports = frappe.get_all("Report", {"module": module, "is_standard": "Yes"}, pluck='name')
+ for report in reports:
+ frappe.delete_doc("Report", report, ignore_missing=True, force=True)
+
+ dashboards = frappe.get_all("Dashboard", {"module": module, "is_standard": 1}, pluck='name')
+ for dashboard in dashboards:
+ frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True)
+
+ doctypes = frappe.get_all("DocType", {"module": module, "custom": 0}, pluck='name')
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
+
+ custom_fields = [
+ {"dt": "Sales Invoice", "fieldname": "restaurant"},
+ {"dt": "Sales Invoice", "fieldname": "restaurant_table"},
+ {"dt": "Price List", "fieldname": "restaurant_menu"},
+ ]
+
+ for field in custom_fields:
+ custom_field = frappe.db.get_value("Custom Field", field)
+ frappe.delete_doc("Custom Field", custom_field, ignore_missing=True)
diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
new file mode 100644
index 0000000000..c4f097fdd9
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
@@ -0,0 +1,48 @@
+import frappe
+from frappe.utils import today
+
+
+def execute():
+ for dt in ("cost_center_allocation", "cost_center_allocation_percentage"):
+ frappe.reload_doc('accounts', 'doctype', dt)
+
+ cc_allocations = get_existing_cost_center_allocations()
+ if cc_allocations:
+ create_new_cost_center_allocation_records(cc_allocations)
+
+ frappe.delete_doc('DocType', 'Distributed Cost Center', ignore_missing=True)
+
+def create_new_cost_center_allocation_records(cc_allocations):
+ for main_cc, allocations in cc_allocations.items():
+ cca = frappe.new_doc("Cost Center Allocation")
+ cca.main_cost_center = main_cc
+ cca.valid_from = today()
+
+ for child_cc, percentage in allocations.items():
+ cca.append("allocation_percentages", ({
+ "cost_center": child_cc,
+ "percentage": percentage
+ }))
+ cca.save()
+ cca.submit()
+
+def get_existing_cost_center_allocations():
+ if not frappe.db.exists("DocType", "Distributed Cost Center"):
+ return
+
+ par = frappe.qb.DocType("Cost Center")
+ child = frappe.qb.DocType("Distributed Cost Center")
+
+ records = (
+ frappe.qb.from_(par)
+ .inner_join(child).on(par.name == child.parent)
+ .select(par.name, child.cost_center, child.percentage_allocation)
+ .where(par.enable_distributed_cost_center == 1)
+ ).run(as_dict=True)
+
+ cc_allocations = frappe._dict()
+ for d in records:
+ cc_allocations.setdefault(d.name, frappe._dict())\
+ .setdefault(d.cost_center, d.percentage_allocation)
+
+ return cc_allocations
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py
index 30d3ea0cb1..0c7785367c 100644
--- a/erpnext/patches/v14_0/migrate_crm_settings.py
+++ b/erpnext/patches/v14_0/migrate_crm_settings.py
@@ -9,8 +9,9 @@ def execute():
], as_dict=True)
frappe.reload_doc('crm', 'doctype', 'crm_settings')
- frappe.db.set_value('CRM Settings', 'CRM Settings', {
- 'campaign_naming_by': settings.campaign_naming_by,
- 'close_opportunity_after_days': settings.close_opportunity_after_days,
- 'default_valid_till': settings.default_valid_till
- })
+ if settings:
+ frappe.db.set_value('CRM Settings', 'CRM Settings', {
+ 'campaign_naming_by': settings.campaign_naming_by,
+ 'close_opportunity_after_days': settings.close_opportunity_after_days,
+ 'default_valid_till': settings.default_valid_till
+ })
diff --git a/erpnext/patches/v14_0/rearrange_company_fields.py b/erpnext/patches/v14_0/rearrange_company_fields.py
new file mode 100644
index 0000000000..fd7eb7f750
--- /dev/null
+++ b/erpnext/patches/v14_0/rearrange_company_fields.py
@@ -0,0 +1,28 @@
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ custom_fields = {
+ 'Company': [
+ dict(fieldname='hra_section', label='HRA Settings',
+ fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ dict(fieldname='basic_component', label='Basic Component',
+ fieldtype='Link', options='Salary Component', insert_after='hra_section'),
+ dict(fieldname='hra_component', label='HRA Component',
+ fieldtype='Link', options='Salary Component', insert_after='basic_component'),
+ dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
+ dict(fieldname='arrear_component', label='Arrear Component',
+ fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
+ dict(fieldname='non_profit_section', label='Non Profit Settings',
+ fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
+ dict(fieldname='company_80g_number', label='80G Number',
+ fieldtype='Data', insert_after='non_profit_section'),
+ dict(fieldname='with_effect_from', label='80G With Effect From',
+ fieldtype='Date', insert_after='company_80g_number'),
+ dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
+ dict(fieldname='pan_details', label='PAN Number',
+ fieldtype='Data', insert_after='non_profit_column_break')
+ ]
+ }
+
+ create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/patches/v14_0/restore_einvoice_fields.py b/erpnext/patches/v14_0/restore_einvoice_fields.py
new file mode 100644
index 0000000000..c4431fb9db
--- /dev/null
+++ b/erpnext/patches/v14_0/restore_einvoice_fields.py
@@ -0,0 +1,24 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+from erpnext.regional.india.setup import add_permissions, add_print_formats
+
+
+def execute():
+ # restores back the 2 custom fields that was deleted while removing e-invoicing from v14
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
+ add_permissions()
+ add_print_formats()
diff --git a/erpnext/patches/v14_0/update_leave_notification_template.py b/erpnext/patches/v14_0/update_leave_notification_template.py
new file mode 100644
index 0000000000..e744054a2f
--- /dev/null
+++ b/erpnext/patches/v14_0/update_leave_notification_template.py
@@ -0,0 +1,17 @@
+import os
+
+import frappe
+from frappe import _
+
+
+def execute():
+ base_path = frappe.get_app_path("erpnext", "hr", "doctype")
+ response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html"))
+
+ template = frappe.db.exists("Email Template", _("Leave Approval Notification"))
+ if template:
+ frappe.db.set_value("Email Template", template, "response", response)
+
+ template = frappe.db.exists("Email Template", _("Leave Status Notification"))
+ if template:
+ frappe.db.set_value("Email Template", template, "response", response)
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json
index d9efe458dc..9c897a7c69 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.json
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json
@@ -204,10 +204,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-05-26 11:10:00.812698",
+ "modified": "2022-01-19 12:56:51.765353",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Additional Salary",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -239,8 +240,10 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "employee",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
index 83326975b0..2e4b64e9da 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
@@ -147,10 +147,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:35:08.940087",
+ "modified": "2022-01-19 12:58:31.664468",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -212,8 +213,10 @@
}
],
"quick_entry": 1,
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
index b3bac01818..5deb0a5eca 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
@@ -144,10 +144,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:37:21.024625",
+ "modified": "2022-01-19 12:59:15.699118",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Claim",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -208,8 +209,10 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
index 0d10b2c19a..64fb8c5c98 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
@@ -94,10 +94,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:38:20.332316",
+ "modified": "2022-01-19 12:52:19.850710",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Incentive",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -136,8 +137,10 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_other_income/employee_other_income.json b/erpnext/payroll/doctype/employee_other_income/employee_other_income.json
index 14f63e4fdd..04ce9f79a1 100644
--- a/erpnext/payroll/doctype/employee_other_income/employee_other_income.json
+++ b/erpnext/payroll/doctype/employee_other_income/employee_other_income.json
@@ -76,10 +76,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-06-22 22:55:17.604688",
+ "modified": "2022-01-19 12:58:43.255900",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Other Income",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -129,7 +130,10 @@
}
],
"quick_entry": 1,
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
index b247d266ae..5ef373e887 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
@@ -119,10 +119,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:39:59.237361",
+ "modified": "2022-01-19 12:58:54.707871",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Declaration",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -186,7 +187,10 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
index 77b107ef4a..bb90051e5d 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
@@ -142,10 +142,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:41:13.723339",
+ "modified": "2022-01-19 12:58:24.244546",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Proof Submission",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -209,7 +210,10 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json
index 48a9ce4759..197089567d 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.json
+++ b/erpnext/payroll/doctype/gratuity/gratuity.json
@@ -167,10 +167,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-07-02 15:05:57.396398",
+ "modified": "2022-01-19 12:54:37.306145",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Gratuity",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -198,6 +199,9 @@
"write": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name"
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
index 7ea6210c7a..f8d8bb46de 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
@@ -105,10 +105,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:43:28.363644",
+ "modified": "2022-01-19 12:57:37.898953",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Retention Bonus",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -163,7 +164,10 @@
"share": 1
}
],
+ "search_fields": "employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 4e40e13be0..fe8e22cedf 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -637,7 +637,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2021-12-23 11:47:47.098248",
+ "modified": "2022-01-19 12:45:54.999345",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
@@ -673,9 +673,11 @@
"role": "Employee"
}
],
+ "search_fields": "employee_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "employee",
"title_field": "employee_name"
-}
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f33443d0d7..f727ff4378 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -746,11 +746,12 @@ class SalarySlip(TransactionBase):
previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component)
# get taxable_earnings for current period (all days)
- current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption)
+ current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption, payroll_period=payroll_period)
future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1)
# get taxable_earnings, addition_earnings for current actual payment days
- current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1)
+ current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption,
+ based_on_payment_days=1, payroll_period=payroll_period)
current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings
current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income
current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax
@@ -876,7 +877,7 @@ class SalarySlip(TransactionBase):
return total_tax_paid
- def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0):
+ def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None):
joining_date, relieving_date = self.get_joining_and_relieving_dates()
taxable_earnings = 0
@@ -903,7 +904,7 @@ class SalarySlip(TransactionBase):
# Get additional amount based on future recurring additional salary
if additional_amount and earning.is_recurring_additional_salary:
additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
- earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month
+ earning.additional_amount, payroll_period) # Used earning.additional_amount to consider the amount for the full month
if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
@@ -920,7 +921,7 @@ class SalarySlip(TransactionBase):
if additional_amount and ded.is_recurring_additional_salary:
additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
- ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month
+ ded.additional_amount, payroll_period) # Used ded.additional_amount to consider the amount for the full month
return frappe._dict({
"taxable_earnings": taxable_earnings,
@@ -929,12 +930,18 @@ class SalarySlip(TransactionBase):
"flexi_benefits": flexi_benefits
})
- def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
+ def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount, payroll_period):
future_recurring_additional_amount = 0
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
# future month count excluding current
from_date, to_date = getdate(self.start_date), getdate(to_date)
+
+ # If recurring period end date is beyond the payroll period,
+ # last day of payroll period should be considered for recurring period calculation
+ if getdate(to_date) > getdate(payroll_period.end_date):
+ to_date = getdate(payroll_period.end_date)
+
future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
if future_recurring_period > 0:
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index c0e005ad99..597fd5a250 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -147,7 +147,7 @@ class TestSalarySlip(unittest.TestCase):
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
- emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
+ emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
# mark attendance
@@ -994,6 +994,8 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non
))
leave_application.submit()
+ return leave_application
+
def setup_test():
make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json
index 5dd1d701f0..8df995769d 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.json
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json
@@ -58,6 +58,7 @@
"width": "50%"
},
{
+ "allow_on_submit": 1,
"default": "Yes",
"fieldname": "is_active",
"fieldtype": "Select",
@@ -232,10 +233,11 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 15:41:12.342380",
+ "modified": "2022-02-03 23:50:10.205676",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -271,5 +273,6 @@
],
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index 197ab5f25b..613246e732 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -162,7 +162,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-12-23 17:28:09.794444",
+ "modified": "2022-01-19 12:43:54.439073",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",
@@ -209,6 +209,7 @@
"write": 1
}
],
+ "search_fields": "employee_name, salary_structure",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
diff --git a/erpnext/payroll/workspace/payroll/payroll.json b/erpnext/payroll/workspace/payroll/payroll.json
index 7246dae5bc..762bea02c7 100644
--- a/erpnext/payroll/workspace/payroll/payroll.json
+++ b/erpnext/payroll/workspace/payroll/payroll.json
@@ -5,7 +5,7 @@
"label": "Outgoing Salary"
}
],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Payroll\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Structure\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payroll Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Slip\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Income Tax Slab\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Register\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payroll\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Compensations\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Payroll\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Salary Structure\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payroll Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Salary Slip\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Income Tax Slab\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Salary Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payroll\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Compensations\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-05-27 19:54:23.405607",
"docstatus": 0,
"doctype": "Workspace",
@@ -312,7 +312,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.335325",
+ "modified": "2022-01-13 17:41:19.098813",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll",
@@ -321,7 +321,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 19,
+ "sequence_id": 19.0,
"shortcuts": [
{
"label": "Salary Structure",
diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js
index c7c66e0055..59f808a315 100644
--- a/erpnext/portal/doctype/homepage/homepage.js
+++ b/erpnext/portal/doctype/homepage/homepage.js
@@ -3,9 +3,9 @@
frappe.ui.form.on('Homepage', {
setup: function(frm) {
- frm.fields_dict["products"].grid.get_field("item_code").get_query = function(){
+ frm.fields_dict["products"].grid.get_field("item").get_query = function() {
return {
- filters: {'show_in_website': 1}
+ filters: {'published': 1}
}
}
},
@@ -21,11 +21,10 @@ 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 && frm.doc.products_url){
- window.location.href = frm.doc.products_url + '/' + encodeURIComponent(child.item_code);
+ 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 ad27278dc6..73f816d4d4 100644
--- a/erpnext/portal/doctype/homepage/homepage.json
+++ b/erpnext/portal/doctype/homepage/homepage.json
@@ -1,518 +1,143 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
+ "actions": [],
"beta": 1,
"creation": "2016-04-22 05:27:52.109319",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "company",
+ "hero_section_based_on",
+ "column_break_2",
+ "title",
+ "section_break_4",
+ "tag_line",
+ "description",
+ "hero_image",
+ "slideshow",
+ "hero_section",
+ "products_section",
+ "products_url",
+ "products"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "company",
"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": "Company",
- "length": 0,
- "no_copy": 0,
"options": "Company",
- "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,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "hero_section_based_on",
"fieldtype": "Select",
- "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": "Hero Section Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Default\nSlideshow\nHomepage Section",
- "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
+ "options": "Default\nSlideshow\nHomepage Section"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "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
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "title",
"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": "Title",
- "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
+ "label": "Title"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "section_break_4",
"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": "Hero Section",
- "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
+ "label": "Hero Section"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
"description": "Company Tagline for website homepage",
"fieldname": "tag_line",
"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": "Tag Line",
- "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,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
"description": "Company Description for website homepage",
"fieldname": "description",
"fieldtype": "Text",
- "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": "Description",
- "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,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
"fieldname": "hero_image",
"fieldtype": "Attach Image",
- "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": "Hero Image",
- "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
+ "label": "Hero Image"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Slideshow'",
- "description": "",
"fieldname": "slideshow",
"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": "Homepage Slideshow",
- "length": 0,
- "no_copy": 0,
- "options": "Website Slideshow",
- "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
+ "options": "Website Slideshow"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Homepage Section'",
"fieldname": "hero_section",
"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": "Homepage Section",
- "length": 0,
- "no_copy": 0,
- "options": "Homepage Section",
- "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
+ "options": "Homepage Section"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "products_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": "Products",
- "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
+ "label": "Products"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "/products",
+ "default": "/all-products",
"fieldname": "products_url",
"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": "URL for \"All Products\"",
- "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
+ "label": "URL for \"All Products\""
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "Products to be shown on website homepage",
"fieldname": "products",
"fieldtype": "Table",
- "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": "Products",
- "length": 0,
- "no_copy": 0,
"options": "Homepage Featured Product",
- "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,
"width": "40px"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
"issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-03-02 23:12:59.676202",
+ "links": [],
+ "modified": "2021-02-18 13:29:29.531639",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
- "report": 0,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
- "report": 0,
"role": "Administrator",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "company",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "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 1e056a6dac..8092ba208a 100644
--- a/erpnext/portal/doctype/homepage/homepage.py
+++ b/erpnext/portal/doctype/homepage/homepage.py
@@ -14,12 +14,14 @@ class Homepage(Document):
delete_page_cache('home')
def setup_items(self):
- for d in frappe.get_all('Item', fields=['name', 'item_name', 'description', 'image'],
- filters={'show_in_website': 1}, limit=3):
+ for d in frappe.get_all('Website Item', fields=['name', 'item_name', 'description', 'image', 'route'],
+ filters={'published': 1}, limit=3):
- doc = frappe.get_doc('Item', d.name)
+ 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.image))
+ item_name=d.item_name, description=d.description,
+ image=d.image, route=d.route))
+
diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
index 01c32efec9..63789e35b5 100644
--- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
+++ b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
@@ -25,10 +25,10 @@
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
- "label": "Item Code",
+ "label": "Item",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
- "options": "Item",
+ "options": "Website Item",
"print_width": "150px",
"reqd": 1,
"search_index": 1,
@@ -63,7 +63,7 @@
"collapsible": 1,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
- "label": "Description"
+ "label": "Details"
},
{
"fetch_from": "item_code.web_long_description",
@@ -89,12 +89,14 @@
"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",
@@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-08-25 15:27:49.573537",
+ "modified": "2021-02-18 13:05:50.669311",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage Featured Product",
diff --git a/erpnext/portal/doctype/products_settings/products_settings.js b/erpnext/portal/doctype/products_settings/products_settings.js
deleted file mode 100644
index 2f8b037164..0000000000
--- a/erpnext/portal/doctype/products_settings/products_settings.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Products Settings', {
- refresh: function(frm) {
- frappe.model.with_doctype('Item', () => {
- const item_meta = frappe.get_meta('Item');
-
- const valid_fields = item_meta.fields.filter(
- df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
- ).map(df => ({ label: df.label, value: df.fieldname }));
-
- frm.fields_dict.filter_fields.grid.update_docfield_property(
- 'fieldname', 'fieldtype', 'Select'
- );
- frm.fields_dict.filter_fields.grid.update_docfield_property(
- 'fieldname', 'options', valid_fields
- );
- });
- }
-});
diff --git a/erpnext/portal/doctype/products_settings/products_settings.json b/erpnext/portal/doctype/products_settings/products_settings.json
deleted file mode 100644
index 2cf8431497..0000000000
--- a/erpnext/portal/doctype/products_settings/products_settings.json
+++ /dev/null
@@ -1,389 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-04-22 09:11:55.272398",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "If checked, the Home page will be the default Item Group for the website",
- "fieldname": "home_page_is_products",
- "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": "Home Page is Products",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "show_availability_status",
- "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": "Show Availability Status",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_5",
- "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": "Product Page",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "6",
- "fieldname": "products_per_page",
- "fieldtype": "Int",
- "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": "Products per Page",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "enable_field_filters",
- "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": "Enable Field Filters",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "enable_field_filters",
- "fieldname": "filter_fields",
- "fieldtype": "Table",
- "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": "Item Fields",
- "length": 0,
- "no_copy": 0,
- "options": "Website Filter Field",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "enable_attribute_filters",
- "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": "Enable Attribute Filters",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "enable_attribute_filters",
- "fieldname": "filter_attributes",
- "fieldtype": "Table",
- "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": "Attributes",
- "length": 0,
- "no_copy": 0,
- "options": "Website Attribute",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "hide_variants",
- "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": "Hide Variants",
- "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
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-03-07 19:18:31.822309",
- "modified_by": "Administrator",
- "module": "Portal",
- "name": "Products Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "Website 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,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/products_settings/products_settings.py b/erpnext/portal/doctype/products_settings/products_settings.py
deleted file mode 100644
index 0e106c634b..0000000000
--- a/erpnext/portal/doctype/products_settings/products_settings.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import cint
-
-
-class ProductsSettings(Document):
- def validate(self):
- if self.home_page_is_products:
- frappe.db.set_value("Website Settings", None, "home_page", "products")
- elif frappe.db.get_single_value("Website Settings", "home_page") == 'products':
- frappe.db.set_value("Website Settings", None, "home_page", "home")
-
- self.validate_field_filters()
- self.validate_attribute_filters()
- frappe.clear_document_cache("Product Settings", "Product Settings")
-
- def validate_field_filters(self):
- if not (self.enable_field_filters and self.filter_fields): return
-
- item_meta = frappe.get_meta('Item')
- valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ['Link', 'Table MultiSelect']]
-
- for f in self.filter_fields:
- if f.fieldname not in valid_fields:
- frappe.throw(_('Filter Fields Row #{0}: Fieldname
{1} must be of type "Link" or "Table MultiSelect"').format(f.idx, f.fieldname))
-
- def validate_attribute_filters(self):
- if not (self.enable_attribute_filters and self.filter_attributes): return
-
- # if attribute filters are enabled, hide_variants should be disabled
- self.hide_variants = 0
-
-
-def home_page_is_products(doc, method):
- '''Called on saving Website Settings'''
- home_page_is_products = cint(frappe.db.get_single_value('Products Settings', 'home_page_is_products'))
- if home_page_is_products:
- doc.home_page = 'products'
diff --git a/erpnext/portal/doctype/products_settings/test_products_settings.py b/erpnext/portal/doctype/products_settings/test_products_settings.py
deleted file mode 100644
index 66026fc046..0000000000
--- a/erpnext/portal/doctype/products_settings/test_products_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestProductsSettings(unittest.TestCase):
- pass
diff --git a/erpnext/portal/doctype/website_attribute/website_attribute.json b/erpnext/portal/doctype/website_attribute/website_attribute.json
index 2874dc432c..eed33ec10e 100644
--- a/erpnext/portal/doctype/website_attribute/website_attribute.json
+++ b/erpnext/portal/doctype/website_attribute/website_attribute.json
@@ -1,76 +1,32 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2019-01-01 13:04:54.479079",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2019-01-01 13:04:54.479079",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "attribute"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "attribute",
- "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": "Attribute",
- "length": 0,
- "no_copy": 0,
- "options": "Item Attribute",
- "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,
- "translatable": 0,
- "unique": 0
+ "fieldname": "attribute",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Attribute",
+ "options": "Item Attribute",
+ "reqd": 1
}
- ],
- "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": "2019-01-01 13:04:59.715572",
- "modified_by": "Administrator",
- "module": "Portal",
- "name": "Website Attribute",
- "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": "2021-02-18 13:18:57.810536",
+ "modified_by": "Administrator",
+ "module": "Portal",
+ "name": "Website Attribute",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py
deleted file mode 100644
index b478489920..0000000000
--- a/erpnext/portal/product_configurator/test_product_configurator.py
+++ /dev/null
@@ -1,143 +0,0 @@
-import unittest
-
-import frappe
-from bs4 import BeautifulSoup
-from frappe.utils import get_html_for_route
-
-from erpnext.portal.product_configurator.utils import get_products_for_website
-
-test_dependencies = ["Item"]
-
-class TestProductConfigurator(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- cls.create_variant_item()
-
- @classmethod
- def create_variant_item(cls):
- if not frappe.db.exists('Item', '_Test Variant Item - 2XL'):
- frappe.get_doc({
- "description": "_Test Variant Item - 2XL",
- "item_code": "_Test Variant Item - 2XL",
- "item_name": "_Test Variant Item - 2XL",
- "doctype": "Item",
- "is_stock_item": 1,
- "variant_of": "_Test Variant Item",
- "item_group": "_Test Item Group",
- "stock_uom": "_Test UOM",
- "item_defaults": [{
- "company": "_Test Company",
- "default_warehouse": "_Test Warehouse - _TC",
- "expense_account": "_Test Account Cost for Goods Sold - _TC",
- "buying_cost_center": "_Test Cost Center - _TC",
- "selling_cost_center": "_Test Cost Center - _TC",
- "income_account": "Sales - _TC"
- }],
- "attributes": [
- {
- "attribute": "Test Size",
- "attribute_value": "2XL"
- }
- ],
- "show_variant_in_website": 1
- }).insert()
-
- def create_regular_web_item(self, name, item_group=None):
- if not frappe.db.exists('Item', name):
- doc = frappe.get_doc({
- "description": name,
- "item_code": name,
- "item_name": name,
- "doctype": "Item",
- "is_stock_item": 1,
- "item_group": item_group or "_Test Item Group",
- "stock_uom": "_Test UOM",
- "item_defaults": [{
- "company": "_Test Company",
- "default_warehouse": "_Test Warehouse - _TC",
- "expense_account": "_Test Account Cost for Goods Sold - _TC",
- "buying_cost_center": "_Test Cost Center - _TC",
- "selling_cost_center": "_Test Cost Center - _TC",
- "income_account": "Sales - _TC"
- }],
- "show_in_website": 1
- }).insert()
- else:
- doc = frappe.get_doc("Item", name)
- return doc
-
- def test_product_list(self):
- template_items = frappe.get_all('Item', {'show_in_website': 1})
- variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
-
- products_settings = frappe.get_doc('Products Settings')
- products_settings.enable_field_filters = 1
- products_settings.append('filter_fields', {'fieldname': 'item_group'})
- products_settings.append('filter_fields', {'fieldname': 'stock_uom'})
- products_settings.save()
-
- html = get_html_for_route('all-products')
-
- soup = BeautifulSoup(html, 'html.parser')
- products_list = soup.find(class_='products-list')
- items = products_list.find_all(class_='card')
- self.assertEqual(len(items), len(template_items + variant_items))
-
- items_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_in_website': 1})
- variants_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_variant_in_website': 1})
-
- # mock query params
- frappe.form_dict = frappe._dict({
- 'field_filters': '{"item_group":["_Test Item Group Desktops"]}'
- })
- html = get_html_for_route('all-products')
- soup = BeautifulSoup(html, 'html.parser')
- products_list = soup.find(class_='products-list')
- items = products_list.find_all(class_='card')
- self.assertEqual(len(items), len(items_with_item_group + variants_with_item_group))
-
-
- def test_get_products_for_website(self):
- items = get_products_for_website(attribute_filters={
- 'Test Size': ['2XL']
- })
- self.assertEqual(len(items), 1)
-
- def test_products_in_multiple_item_groups(self):
- """Check if product is visible on multiple item group pages barring its own."""
- from erpnext.shopping_cart.product_query import ProductQuery
-
- if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
- item_group_doc = frappe.get_doc({
- "doctype": "Item Group",
- "item_group_name": "Tech Items",
- "parent_item_group": "All Item Groups",
- "show_in_website": 1
- }).insert()
- else:
- item_group_doc = frappe.get_doc("Item Group", "Tech Items")
-
- doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
- if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
- doc.append("website_item_groups", {
- "item_group": "_Test Item Group Desktops"
- })
- doc.save()
-
- # check if item is visible in its own Item Group's page
- engine = ProductQuery()
- items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
- self.assertEqual(len(items), 1)
- self.assertEqual(items[0].item_code, "Portal Item")
-
- # check if item is visible in configured foreign Item Group's page
- engine = ProductQuery()
- items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
- item_codes = [row.item_code for row in items]
-
- self.assertIn(len(items), [2, 3])
- self.assertIn("Portal Item", item_codes)
-
- # teardown
- doc.delete()
- item_group_doc.delete()
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
deleted file mode 100644
index cf623c8d42..0000000000
--- a/erpnext/portal/product_configurator/utils.py
+++ /dev/null
@@ -1,446 +0,0 @@
-import frappe
-from frappe.utils import cint
-
-from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
-from erpnext.setup.doctype.item_group.item_group import get_child_groups
-from erpnext.shopping_cart.product_info import get_product_info_for_website
-
-
-def get_field_filter_data():
- product_settings = get_product_settings()
- filter_fields = [row.fieldname for row in product_settings.filter_fields]
-
- meta = frappe.get_meta('Item')
- fields = [df for df in meta.fields if df.fieldname in filter_fields]
-
- filter_data = []
- for f in fields:
- doctype = f.get_link_doctype()
-
- # apply enable/disable/show_in_website filter
- meta = frappe.get_meta(doctype)
- filters = {}
- if meta.has_field('enabled'):
- filters['enabled'] = 1
- if meta.has_field('disabled'):
- filters['disabled'] = 0
- if meta.has_field('show_in_website'):
- filters['show_in_website'] = 1
-
- values = [d.name for d in frappe.get_all(doctype, filters)]
- filter_data.append([f, values])
-
- return filter_data
-
-
-def get_attribute_filter_data():
- product_settings = get_product_settings()
- attributes = [row.attribute for row in product_settings.filter_attributes]
- attribute_docs = [
- frappe.get_doc('Item Attribute', attribute) for attribute in attributes
- ]
-
- # mark attribute values as checked if they are present in the request url
- if frappe.form_dict:
- for attr in attribute_docs:
- if attr.name in frappe.form_dict:
- value = frappe.form_dict[attr.name]
- if value:
- enabled_values = value.split(',')
- else:
- enabled_values = []
-
- for v in enabled_values:
- for item_attribute_row in attr.item_attribute_values:
- if v == item_attribute_row.attribute_value:
- item_attribute_row.checked = True
-
- return attribute_docs
-
-
-def get_products_for_website(field_filters=None, attribute_filters=None, search=None):
- if attribute_filters:
- item_codes = get_item_codes_by_attributes(attribute_filters)
- items_by_attributes = get_items([['name', 'in', item_codes]])
-
- if field_filters:
- items_by_fields = get_items_by_fields(field_filters)
-
- if attribute_filters and not field_filters:
- return items_by_attributes
-
- if field_filters and not attribute_filters:
- return items_by_fields
-
- if field_filters and attribute_filters:
- items_intersection = []
- item_codes_in_attribute = [item.name for item in items_by_attributes]
-
- for item in items_by_fields:
- if item.name in item_codes_in_attribute:
- items_intersection.append(item)
-
- return items_intersection
-
- if search:
- return get_items(search=search)
-
- return get_items()
-
-
-@frappe.whitelist(allow_guest=True)
-def get_products_html_for_website(field_filters=None, attribute_filters=None):
- field_filters = frappe.parse_json(field_filters)
- attribute_filters = frappe.parse_json(attribute_filters)
- set_item_group_filters(field_filters)
-
- items = get_products_for_website(field_filters, attribute_filters)
- html = ''.join(get_html_for_items(items))
-
- if not items:
- html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
-
- return html
-
-def set_item_group_filters(field_filters):
- if field_filters is not None and 'item_group' in field_filters:
- field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
-
-
-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)])
- 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):
- '''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.shopping_cart.product_info import get_product_info_for_website
- if exact_match:
- data = get_product_info_for_website(exact_match[0])
- product_info = data.product_info
- if product_info:
- product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
- if not data.cart_settings.show_price:
- product_info = None
- else:
- product_info = None
-
- 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
- }
-
-
-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)
-
-
-def get_items_by_fields(field_filters):
- meta = frappe.get_meta('Item')
- filters = []
- for fieldname, values in field_filters.items():
- if not values: continue
-
- _doctype = 'Item'
- _fieldname = fieldname
-
- df = meta.get_field(fieldname)
- if df.fieldtype == 'Table MultiSelect':
- child_doctype = df.options
- child_meta = frappe.get_meta(child_doctype)
- fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 })
- if fields:
- _doctype = child_doctype
- _fieldname = fields[0].fieldname
-
- if len(values) == 1:
- filters.append([_doctype, _fieldname, '=', values[0]])
- else:
- filters.append([_doctype, _fieldname, 'in', values])
-
- return get_items(filters)
-
-
-def get_items(filters=None, search=None):
- start = frappe.form_dict.get('start', 0)
- products_settings = get_product_settings()
- page_length = products_settings.products_per_page
-
- filters = filters or []
- # convert to list of filters
- if isinstance(filters, dict):
- filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()]
-
- enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and')
-
- show_in_website_condition = ''
- if products_settings.hide_variants:
- show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and')
- else:
- show_in_website_condition = get_conditions([
- ['show_in_website', '=', 1],
- ['show_variant_in_website', '=', 1]
- ], 'or')
-
- search_condition = ''
- if search:
- # Default fields to search from
- default_fields = {'name', 'item_name', 'description', 'item_group'}
-
- # Get meta search fields
- meta = frappe.get_meta("Item")
- meta_fields = set(meta.get_search_fields())
-
- # Join the meta fields and default fields set
- search_fields = default_fields.union(meta_fields)
- try:
- if frappe.db.count('Item', cache=True) > 50000:
- search_fields.remove('description')
- except KeyError:
- pass
-
- # Build or filters for query
- search = '%{}%'.format(search)
- or_filters = [[field, 'like', search] for field in search_fields]
-
- search_condition = get_conditions(or_filters, 'or')
-
- filter_condition = get_conditions(filters, 'and')
-
- where_conditions = ' and '.join(
- [condition for condition in [enabled_items_filter, show_in_website_condition, \
- search_condition, filter_condition] if condition]
- )
-
- left_joins = []
- for f in filters:
- if len(f) == 4 and f[0] != 'Item':
- left_joins.append(f[0])
-
- left_join = ' '.join(['LEFT JOIN `tab{0}` on (`tab{0}`.parent = `tabItem`.name)'.format(l) for l in left_joins])
-
- results = frappe.db.sql('''
- SELECT
- `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
- `tabItem`.`website_image`, `tabItem`.`image`,
- `tabItem`.`web_long_description`, `tabItem`.`description`,
- `tabItem`.`route`, `tabItem`.`item_group`
- FROM
- `tabItem`
- {left_join}
- WHERE
- {where_conditions}
- GROUP BY
- `tabItem`.`name`
- ORDER BY
- `tabItem`.`weightage` DESC
- LIMIT
- {page_length}
- OFFSET
- {start}
- '''.format(
- where_conditions=where_conditions,
- start=start,
- page_length=page_length,
- left_join=left_join
- )
- , as_dict=1)
-
- for r in results:
- r.description = r.web_long_description or r.description
- r.image = r.website_image or r.image
- product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
- if product_info:
- r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
-
- return results
-
-
-def get_conditions(filter_list, and_or='and'):
- from frappe.model.db_query import DatabaseQuery
-
- if not filter_list:
- return ''
-
- conditions = []
- DatabaseQuery('Item').build_filter_conditions(filter_list, conditions, ignore_permissions=True)
- join_by = ' {0} '.format(and_or)
-
- return '(' + join_by.join(conditions) + ')'
-
-# 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_html_for_items(items):
- html = []
- for item in items:
- html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
- 'item': item
- }))
- return html
-
-def get_product_settings():
- doc = frappe.get_cached_doc('Products Settings')
- doc.products_per_page = doc.products_per_page or 20
- return doc
diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py
index 974b51ef0a..24bcab445a 100644
--- a/erpnext/portal/utils.py
+++ b/erpnext/portal/utils.py
@@ -1,10 +1,10 @@
import frappe
from frappe.utils.nestedset import get_root_of
-from erpnext.shopping_cart.cart import get_debtors_account
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+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):
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 2570df7026..1cda0a08c4 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -235,13 +235,13 @@
{
"fieldname": "actual_start_date",
"fieldtype": "Data",
- "label": "Actual Start Date",
+ "label": "Actual Start Date (via Time Sheet)",
"read_only": 1
},
{
"fieldname": "actual_time",
"fieldtype": "Float",
- "label": "Actual Time (in Hours)",
+ "label": "Actual Time (in Hours via Time Sheet)",
"read_only": 1
},
{
@@ -251,7 +251,7 @@
{
"fieldname": "actual_end_date",
"fieldtype": "Date",
- "label": "Actual End Date",
+ "label": "Actual End Date (via Time Sheet)",
"oldfieldname": "act_completion_date",
"oldfieldtype": "Date",
"read_only": 1
@@ -458,10 +458,11 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2021-04-28 16:36:11.654632",
+ "modified": "2022-01-29 13:58:27.712714",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -499,6 +500,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index ef4740d9ee..81822085a5 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -249,7 +249,7 @@
{
"fieldname": "actual_time",
"fieldtype": "Float",
- "label": "Actual Time (in hours)",
+ "label": "Actual Time (in Hours via Time Sheet)",
"read_only": 1
},
{
@@ -397,10 +397,11 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
- "modified": "2021-04-16 12:46:51.556741",
+ "modified": "2022-01-29 13:58:47.005241",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
+ "naming_rule": "Expression (old style)",
"nsm_parent_field": "parent_task",
"owner": "Administrator",
"permissions": [
@@ -421,6 +422,7 @@
"show_preview_popup": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "project",
"title_field": "subject",
"track_seen": 1
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index 148d8ba29c..989bcd1670 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -5,7 +5,7 @@ import datetime
import unittest
import frappe
-from frappe.utils import add_months, now_datetime, nowdate
+from frappe.utils import add_months, add_to_date, now_datetime, nowdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -151,6 +151,27 @@ class TestTimesheet(unittest.TestCase):
settings.ignore_employee_time_overlap = initial_setting
settings.save()
+ def test_to_time(self):
+ emp = make_employee("test_employee_6@salary.com")
+ from_time = now_datetime()
+
+ timesheet = frappe.new_doc("Timesheet")
+ timesheet.employee = emp
+ timesheet.append(
+ 'time_logs',
+ {
+ "billable": 1,
+ "activity_type": "_Test Activity Type",
+ "from_time": from_time,
+ "hours": 2,
+ "company": "_Test Company"
+ }
+ )
+ timesheet.save()
+
+ to_time = timesheet.time_logs[0].to_time
+ self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True))
+
def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test"
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index e92785e06c..dd0b5f90f4 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import flt, getdate, time_diff_in_hours
+from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
from erpnext.controllers.queries import get_match_cond
from erpnext.hr.utils import validate_active_employee
@@ -136,10 +136,19 @@ class Timesheet(Document):
def validate_time_logs(self):
for data in self.get('time_logs'):
+ self.set_to_time(data)
self.validate_overlap(data)
self.set_project(data)
self.validate_project(data)
+ def set_to_time(self, data):
+ if not (data.from_time and data.hours):
+ return
+
+ _to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
+ if data.to_time != _to_time:
+ data.to_time = _to_time
+
def validate_overlap(self, data):
settings = frappe.get_single('Projects Settings')
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)
diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json
index 1df2b08983..c5a047d5cb 100644
--- a/erpnext/projects/workspace/projects/projects.json
+++ b/erpnext/projects/workspace/projects/projects.json
@@ -5,7 +5,7 @@
"label": "Open Projects"
}
],
- "content": "[{\"type\": \"chart\", \"data\": {\"chart_name\": \"Open Projects\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Task\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Project\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Timesheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Project Billing Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Projects\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Time Tracking\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"chart\",\"data\":{\"chart_name\":\"Open Projects\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Task\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Timesheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project Billing Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:46:04.874669",
"docstatus": 0,
"doctype": "Workspace",
@@ -194,7 +194,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.540147",
+ "modified": "2022-01-13 17:41:55.163878",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",
@@ -203,7 +203,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 20,
+ "sequence_id": 20.0,
"shortcuts": [
{
"color": "Blue",
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index f8e817770d..569910dd9d 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -7,7 +7,8 @@
],
"js/erpnext-web.min.js": [
"public/js/website_utils.js",
- "public/js/shopping_cart.js"
+ "public/js/shopping_cart.js",
+ "public/js/wishlist.js"
],
"css/erpnext-web.css": [
"public/scss/website.scss",
@@ -65,5 +66,11 @@
"js/hierarchy-chart.min.js": [
"public/js/hierarchy_chart/hierarchy_chart_desktop.js",
"public/js/hierarchy_chart/hierarchy_chart_mobile.js"
+ ],
+ "js/e-commerce.min.js": [
+ "e_commerce/product_ui/views.js",
+ "e_commerce/product_ui/grid.js",
+ "e_commerce/product_ui/list.js",
+ "e_commerce/product_ui/search.js"
]
}
diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js
index eb709e5e85..a0f56a2d07 100644
--- a/erpnext/public/js/conf.js
+++ b/erpnext/public/js/conf.js
@@ -21,6 +21,6 @@ $.extend(frappe.breadcrumbs.module_map, {
'Geo': 'Settings',
'Portal': 'Website',
'Utilities': 'Settings',
- 'Shopping Cart': 'Website',
+ 'E-commerce': 'Website',
'Contacts': 'CRM'
});
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index d696ef55ae..54e5daa6bd 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -441,7 +441,7 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
type: "GET",
method: "erpnext.stock.doctype.packed_item.packed_item.get_items_from_product_bundle",
args: {
- args: {
+ row: {
item_code: args.product_bundle,
quantity: args.quantity,
parenttype: frm.doc.doctype,
diff --git a/erpnext/public/js/customer_reviews.js b/erpnext/public/js/customer_reviews.js
new file mode 100644
index 0000000000..e13ded6b48
--- /dev/null
+++ b/erpnext/public/js/customer_reviews.js
@@ -0,0 +1,138 @@
+$(() => {
+ 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 7db6967923..cbe899dc06 100644
--- a/erpnext/public/js/erpnext-web.bundle.js
+++ b/erpnext/public/js/erpnext-web.bundle.js
@@ -1,2 +1,8 @@
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
index 6a923ae423..d14740c106 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -2,8 +2,8 @@
// License: GNU General Public License v3. See license.txt
// shopping cart
-frappe.provide("erpnext.shopping_cart");
-var shopping_cart = erpnext.shopping_cart;
+frappe.provide("erpnext.e_commerce.shopping_cart");
+var shopping_cart = erpnext.e_commerce.shopping_cart;
var getParams = function (url) {
var params = [];
@@ -51,10 +51,10 @@ frappe.ready(function() {
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.bind_dropdown_cart_buttons();
shopping_cart.show_cart_navbar();
});
@@ -63,7 +63,7 @@ $.extend(shopping_cart, {
$(".shopping-cart").on('shown.bs.dropdown', function() {
if (!$('.shopping-cart-menu .cart-container').length) {
return frappe.call({
- method: 'erpnext.shopping_cart.cart.get_shopping_cart_menu',
+ method: 'erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu',
callback: function(r) {
if (r.message) {
$('.shopping-cart-menu').html(r.message);
@@ -75,15 +75,18 @@ $.extend(shopping_cart, {
},
update_cart: function(opts) {
- if(frappe.session.user==="Guest") {
- if(localStorage) {
+ if (frappe.session.user==="Guest") {
+ if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname);
}
- window.location.href = "/login";
+ 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.shopping_cart.cart.update_cart",
+ method: "erpnext.e_commerce.shopping_cart.cart.update_cart",
args: {
item_code: opts.item_code,
qty: opts.qty,
@@ -92,10 +95,8 @@ $.extend(shopping_cart, {
},
btn: opts.btn,
callback: function(r) {
- shopping_cart.set_cart_count();
- if (r.message.shopping_cart_menu) {
- $('.shopping-cart-menu').html(r.message.shopping_cart_menu);
- }
+ shopping_cart.unfreeze();
+ shopping_cart.set_cart_count(true);
if(opts.callback)
opts.callback(r);
}
@@ -103,7 +104,9 @@ $.extend(shopping_cart, {
}
},
- set_cart_count: function() {
+ 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;
@@ -118,24 +121,37 @@ $.extend(shopping_cart, {
if(parseInt(cart_count) === 0 || cart_count === undefined) {
$cart.css("display", "none");
- $(".cart-items").html('Cart is Empty');
$(".cart-tax-items").hide();
$(".btn-place-order").hide();
- $(".cart-addresses").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}) {
- frappe.freeze();
shopping_cart.update_cart({
item_code,
qty,
@@ -143,10 +159,12 @@ $.extend(shopping_cart, {
with_items: 1,
btn: this,
callback: function(r) {
- frappe.unfreeze();
if(!r.exc) {
$(".cart-items").html(r.message.items);
- $(".cart-tax-items").html(r.message.taxes);
+ $(".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();
}
@@ -155,35 +173,71 @@ $.extend(shopping_cart, {
});
},
-
- bind_dropdown_cart_buttons: function () {
- $(".cart-icon").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 = parseInt(oldValue) + 1;
- } else {
- if (oldValue > 1) {
- newVal = parseInt(oldValue) - 1;
- }
- }
- input.val(newVal);
- var item_code = input.attr("data-item-code");
- shopping_cart.shopping_cart_update({item_code, qty: newVal, cart_dropdown: true});
- return false;
- });
-
- },
-
show_cart_navbar: function () {
frappe.call({
- method: "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.is_cart_enabled",
+ 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/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 597d77c6e9..08270bdea1 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -592,6 +592,6 @@ function check_can_calculate_pending_qty(me) {
&& doc.fg_completed_qty
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
- const itemChecks = !!item;
+ const itemChecks = !!item && !item.allow_alternative_item;
return docChecks && itemChecks;
}
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
new file mode 100644
index 0000000000..f6599e9f6d
--- /dev/null
+++ b/erpnext/public/js/wishlist.js
@@ -0,0 +1,204 @@
+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/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index fef1e76154..4b645b9dde 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -1,16 +1,17 @@
@import "frappe/public/scss/common/mixins";
-body.product-page {
- background: var(--gray-50);
+: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 {
- ol.breadcrumb {
- background-color: var(--gray-50) !important;
- }
-
a {
color: var(--gray-900);
}
@@ -71,9 +72,21 @@ body.product-page {
}
}
+.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: 360px;
+ height: 100%;
align-items: center;
justify-content: center;
@@ -83,6 +96,19 @@ body.product-page {
}
}
+ .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%;
@@ -96,14 +122,28 @@ body.product-page {
.no-image {
@include flex(flex, center, center, null);
- height: 200px;
- margin: 0 auto;
- margin-top: var(--margin-xl);
+ 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);
- width: 80%;
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 {
@@ -136,15 +176,75 @@ body.product-page {
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-all-products {
+#page-index {
.page-header {
font-size: 20px;
font-weight: 700;
@@ -184,28 +284,76 @@ body.product-page {
}
}
+.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));
- min-height: 70vh;
+ background-color: var(--product-bg-color) !important;
+ min-height: fit-content;
.product-details {
- max-width: 40%;
- margin-left: -30px;
+ max-width: 50%;
.btn-add-to-cart {
- font-size: var(--text-base);
+ 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 {
+ stroke: #F47A7A;
+ }
+ }
+
+ .btn-view-in-wishlist {
+ svg use {
+ fill: #F47A7A;
+ stroke: none;
}
}
.product-title {
- font-size: 24px;
+ font-size: 16px;
font-weight: 600;
color: var(--text-color);
- }
-
- .product-code {
- color: var(--text-muted);
- font-size: 13px;
+ padding: 0 !important;
}
.product-description {
@@ -242,7 +390,7 @@ body.product-page {
max-height: 430px;
}
- overflow: scroll;
+ overflow: auto;
}
.item-slideshow-image {
@@ -261,29 +409,116 @@ body.product-page {
.item-cart {
.product-price {
- font-size: 20px;
+ font-size: 22px;
color: var(--text-color);
font-weight: 600;
.formatted-price {
color: var(--text-muted);
- font-size: var(--text-base);
+ 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-header {
- padding: var(--padding-md) var(--padding-xl);
- }
-
.modal-body {
- padding: 0 var(--padding-xl);
padding-bottom: var(--padding-xl);
.status-area {
@@ -323,20 +558,73 @@ body.product-page {
}
}
-.cart-icon {
- .cart-badge {
- position: relative;
- top: -10px;
- left: -12px;
- background: var(--red-600);
- width: 16px;
- align-items: center;
- height: 16px;
- font-size: 10px;
- border-radius: 50%;
+.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 {
+ white-space: nowrap;
+ overflow-x: auto;
+
+ .category-pill {
+ margin: 0px 4px;
+ display: inline-block;
+ padding: 6px 12px;
+ background-color: #ecf5fe;
+ width: fit-content;
+ 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 {
@@ -350,6 +638,7 @@ body.product-page {
display: flex;
flex-direction: column;
justify-content: space-between;
+ height: fit-content;
}
.cart-items-header {
@@ -357,6 +646,10 @@ body.product-page {
}
.cart-table {
+ tr {
+ margin-bottom: 1rem;
+ }
+
th, tr, td {
border-color: var(--border-color);
border-width: 1px;
@@ -374,71 +667,200 @@ body.product-page {
color: var(--text-color);
}
+ .cart-item-image {
+ width: 20%;
+ min-width: 100px;
+ img {
+ max-height: 112px;
+ }
+ }
+
.cart-items {
.item-title {
- font-size: var(--text-base);
+ width: 80%;
+ font-size: 14px;
font-weight: 500;
color: var(--text-color);
}
.item-subtitle {
color: var(--text-muted);
- font-size: var(--text-md);
+ font-size: 13px;
}
.item-subtotal {
- font-size: var(--text-base);
+ 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: var(--text-md);
+ font-size: 13px;
color: var(--text-muted);
}
- textarea {
- width: 40%;
+ .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: 600;
+ 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-addresses {
+ .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: var(--text-md);
+ font-size: 13px;
+ &:disabled {
+ background: var(--gray-100);
+ opacity: 0.65;
+ }
}
}
.place-order-container {
.btn-place-order {
- width: 62%;
+ 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 {
@@ -454,7 +876,7 @@ body.product-page {
.address-card {
.card-title {
- font-size: var(--text-base);
+ font-size: 14px;
font-weight: 500;
}
@@ -463,27 +885,37 @@ body.product-page {
}
.card-text {
- font-size: var(--text-md);
+ font-size: 13px;
color: var(--gray-700);
}
.card-link {
- font-size: var(--text-md);
+ font-size: 13px;
svg use {
- stroke: var(--blue-500);
+ stroke: var(--primary-color);
}
}
.btn-change-address {
- color: var(--blue-500);
+ 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 {
- box-shadow: none;
- color: var(--blue-500) !important;
- border: 1px solid var(--blue-500);
+ color: var(--primary-color) !important;
}
.modal .address-card {
@@ -493,3 +925,451 @@ body.product-page {
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;
+ stroke: #F47A7A !important;
+
+ &:hover {
+ fill: #F47A7A;
+ }
+}
+
+.wished {
+ 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/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json
index ae28470182..3effd59d8e 100644
--- a/erpnext/quality_management/workspace/quality/quality.json
+++ b/erpnext/quality_management/workspace/quality/quality.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Goal\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Procedure\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Inspection\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Review\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Action\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Conformance\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goal and Procedure\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Feedback\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Meeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Review and Action\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Goal\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Procedure\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Inspection\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Review\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Action\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Conformance\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Goal and Procedure\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Feedback\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Meeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Review and Action\",\"col\":4}}]",
"creation": "2020-03-02 15:49:28.632014",
"docstatus": 0,
"doctype": "Workspace",
@@ -142,7 +142,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.699913",
+ "modified": "2022-01-13 17:42:20.105187",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality",
@@ -151,7 +151,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 21,
+ "sequence_id": 21.0,
"shortcuts": [
{
"color": "Grey",
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py
similarity index 100%
rename from erpnext/portal/product_configurator/__init__.py
rename to erpnext/regional/doctype/e_invoice_request_log/__init__.py
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
new file mode 100644
index 0000000000..7b7ba964e5
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('E Invoice Request Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
new file mode 100644
index 0000000000..3034370fea
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
@@ -0,0 +1,102 @@
+{
+ "actions": [],
+ "autoname": "EINV-REQ-.#####",
+ "creation": "2020-12-08 12:54:08.175992",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "url",
+ "headers",
+ "response",
+ "column_break_7",
+ "timestamp",
+ "reference_invoice",
+ "data"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User"
+ },
+ {
+ "fieldname": "reference_invoice",
+ "fieldtype": "Data",
+ "label": "Reference Invoice"
+ },
+ {
+ "fieldname": "headers",
+ "fieldtype": "Code",
+ "label": "Headers",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "label": "Data",
+ "options": "JSON"
+ },
+ {
+ "default": "Now",
+ "fieldname": "timestamp",
+ "fieldtype": "Datetime",
+ "label": "Timestamp"
+ },
+ {
+ "fieldname": "response",
+ "fieldtype": "Code",
+ "label": "Response",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "URL"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-13 12:06:57.253111",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice Request Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
new file mode 100644
index 0000000000..c89552d782
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class EInvoiceRequestLog(Document):
+ pass
diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
new file mode 100644
index 0000000000..091cc88e45
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestEInvoiceRequestLog(unittest.TestCase):
+ pass
diff --git a/erpnext/restaurant/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py
similarity index 100%
rename from erpnext/restaurant/__init__.py
rename to erpnext/regional/doctype/e_invoice_settings/__init__.py
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
new file mode 100644
index 0000000000..54e488610d
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('E Invoice Settings', {
+ refresh(frm) {
+ const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing';
+ frm.dashboard.set_headline(
+ __("Read {0} for more information on E Invoicing features.", [`
documentation `])
+ );
+ }
+});
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
new file mode 100644
index 0000000000..16b2963301
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
@@ -0,0 +1,97 @@
+{
+ "actions": [],
+ "creation": "2020-09-24 16:23:16.235722",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable",
+ "section_break_2",
+ "sandbox_mode",
+ "applicable_from",
+ "credentials",
+ "advanced_settings_section",
+ "client_id",
+ "column_break_8",
+ "client_secret",
+ "auth_token",
+ "token_expiry"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "enable",
+ "fieldtype": "Check",
+ "label": "Enable"
+ },
+ {
+ "depends_on": "enable",
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "auth_token",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "token_expiry",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "credentials",
+ "fieldtype": "Table",
+ "label": "Credentials",
+ "mandatory_depends_on": "enable",
+ "options": "E Invoice User"
+ },
+ {
+ "default": "0",
+ "fieldname": "sandbox_mode",
+ "fieldtype": "Check",
+ "label": "Sandbox Mode"
+ },
+ {
+ "fieldname": "applicable_from",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Applicable From",
+ "reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "advanced_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Advanced Settings"
+ },
+ {
+ "fieldname": "client_id",
+ "fieldtype": "Data",
+ "label": "Client ID"
+ },
+ {
+ "fieldname": "client_secret",
+ "fieldtype": "Password",
+ "label": "Client Secret"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-11-16 19:50:28.029517",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice Settings",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
new file mode 100644
index 0000000000..342d583cc0
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class EInvoiceSettings(Document):
+ def validate(self):
+ if self.enable and not self.credentials:
+ frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.'))
diff --git a/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py
new file mode 100644
index 0000000000..10770deb0e
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestEInvoiceSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/restaurant/doctype/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py
similarity index 100%
rename from erpnext/restaurant/doctype/__init__.py
rename to erpnext/regional/doctype/e_invoice_user/__init__.py
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
new file mode 100644
index 0000000000..a65b1ca7ca
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
@@ -0,0 +1,57 @@
+{
+ "actions": [],
+ "creation": "2020-12-22 15:02:46.229474",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "gstin",
+ "username",
+ "password"
+ ],
+ "fields": [
+ {
+ "fieldname": "gstin",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "GSTIN",
+ "reqd": 1
+ },
+ {
+ "fieldname": "username",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Username",
+ "reqd": 1
+ },
+ {
+ "fieldname": "password",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Password",
+ "reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-22 12:16:56.365616",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice User",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
similarity index 77%
rename from erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py
rename to erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
index dcf0e3b99d..4e0e89c09a 100644
--- a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
@@ -1,10 +1,10 @@
+# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
# import frappe
from frappe.model.document import Document
-class DistributedCostCenter(Document):
+class EInvoiceUser(Document):
pass
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index d48cd67c38..cb79cf8286 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -295,6 +295,10 @@ class GSTR3BReport(Document):
inter_state_supply_details = {}
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
+ gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
+ place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
+ export_type = self.invoice_detail_map.get(inv, {}).get('export_type')
+
for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items:
@@ -302,9 +306,8 @@ class GSTR3BReport(Document):
self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
elif item_code in self.is_non_gst:
self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
- elif rate == 0:
+ elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'):
self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
- #self.report_dict['sup_details']['osup_zero'][key] += tax_amount
else:
if inv in self.cgst_sgst_invoices:
tax_rate = rate/2
@@ -315,9 +318,6 @@ class GSTR3BReport(Document):
self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
- gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
- place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
-
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:
inter_state_supply_details.setdefault((gst_category, place_of_supply), {
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
index 2d1e02eadb..ec271a1029 100644
--- a/erpnext/regional/germany/utils/datev/datev_csv.py
+++ b/erpnext/regional/germany/utils/datev/datev_csv.py
@@ -1,11 +1,11 @@
import datetime
import zipfile
from csv import QUOTE_NONNUMERIC
+from io import BytesIO
import frappe
import pandas as pd
from frappe import _
-from six import BytesIO
from .datev_constants import DataCategory
diff --git a/erpnext/restaurant/doctype/restaurant/__init__.py b/erpnext/regional/india/e_invoice/__init__.py
similarity index 100%
rename from erpnext/restaurant/doctype/restaurant/__init__.py
rename to erpnext/regional/india/e_invoice/__init__.py
diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json
new file mode 100644
index 0000000000..78e56518df
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_item_template.json
@@ -0,0 +1,31 @@
+{{
+ "SlNo": "{item.sr_no}",
+ "PrdDesc": "{item.description}",
+ "IsServc": "{item.is_service_item}",
+ "HsnCd": "{item.gst_hsn_code}",
+ "Barcde": "{item.barcode}",
+ "Unit": "{item.uom}",
+ "Qty": "{item.qty}",
+ "FreeQty": "{item.free_qty}",
+ "UnitPrice": "{item.unit_rate}",
+ "TotAmt": "{item.gross_amount}",
+ "Discount": "{item.discount_amount}",
+ "AssAmt": "{item.taxable_value}",
+ "PrdSlNo": "{item.serial_no}",
+ "GstRt": "{item.tax_rate}",
+ "IgstAmt": "{item.igst_amount}",
+ "CgstAmt": "{item.cgst_amount}",
+ "SgstAmt": "{item.sgst_amount}",
+ "CesRt": "{item.cess_rate}",
+ "CesAmt": "{item.cess_amount}",
+ "CesNonAdvlAmt": "{item.cess_nadv_amount}",
+ "StateCesRt": "{item.state_cess_rate}",
+ "StateCesAmt": "{item.state_cess_amount}",
+ "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
+ "OthChrg": "{item.other_charges}",
+ "TotItemVal": "{item.total_value}",
+ "BchDtls": {{
+ "Nm": "{item.batch_no}",
+ "ExpDt": "{item.batch_expiry_date}"
+ }}
+}}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json
new file mode 100644
index 0000000000..c2a28f2049
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_template.json
@@ -0,0 +1,110 @@
+{{
+ "Version": "1.1",
+ "TranDtls": {{
+ "TaxSch": "{transaction_details.tax_scheme}",
+ "SupTyp": "{transaction_details.supply_type}",
+ "RegRev": "{transaction_details.reverse_charge}",
+ "EcmGstin": "{transaction_details.ecom_gstin}",
+ "IgstOnIntra": "{transaction_details.igst_on_intra}"
+ }},
+ "DocDtls": {{
+ "Typ": "{doc_details.invoice_type}",
+ "No": "{doc_details.invoice_name}",
+ "Dt": "{doc_details.invoice_date}"
+ }},
+ "SellerDtls": {{
+ "Gstin": "{seller_details.gstin}",
+ "LglNm": "{seller_details.legal_name}",
+ "TrdNm": "{seller_details.trade_name}",
+ "Loc": "{seller_details.location}",
+ "Pin": "{seller_details.pincode}",
+ "Stcd": "{seller_details.state_code}",
+ "Addr1": "{seller_details.address_line1}",
+ "Addr2": "{seller_details.address_line2}",
+ "Ph": "{seller_details.phone}",
+ "Em": "{seller_details.email}"
+ }},
+ "BuyerDtls": {{
+ "Gstin": "{buyer_details.gstin}",
+ "LglNm": "{buyer_details.legal_name}",
+ "TrdNm": "{buyer_details.trade_name}",
+ "Addr1": "{buyer_details.address_line1}",
+ "Addr2": "{buyer_details.address_line2}",
+ "Loc": "{buyer_details.location}",
+ "Pin": "{buyer_details.pincode}",
+ "Stcd": "{buyer_details.state_code}",
+ "Ph": "{buyer_details.phone}",
+ "Em": "{buyer_details.email}",
+ "Pos": "{buyer_details.place_of_supply}"
+ }},
+ "DispDtls": {{
+ "Nm": "{dispatch_details.legal_name}",
+ "Addr1": "{dispatch_details.address_line1}",
+ "Addr2": "{dispatch_details.address_line2}",
+ "Loc": "{dispatch_details.location}",
+ "Pin": "{dispatch_details.pincode}",
+ "Stcd": "{dispatch_details.state_code}"
+ }},
+ "ShipDtls": {{
+ "Gstin": "{shipping_details.gstin}",
+ "LglNm": "{shipping_details.legal_name}",
+ "TrdNm": "{shipping_details.trader_name}",
+ "Addr1": "{shipping_details.address_line1}",
+ "Addr2": "{shipping_details.address_line2}",
+ "Loc": "{shipping_details.location}",
+ "Pin": "{shipping_details.pincode}",
+ "Stcd": "{shipping_details.state_code}"
+ }},
+ "ItemList": [
+ {item_list}
+ ],
+ "ValDtls": {{
+ "AssVal": "{invoice_value_details.base_total}",
+ "CgstVal": "{invoice_value_details.total_cgst_amt}",
+ "SgstVal": "{invoice_value_details.total_sgst_amt}",
+ "IgstVal": "{invoice_value_details.total_igst_amt}",
+ "CesVal": "{invoice_value_details.total_cess_amt}",
+ "Discount": "{invoice_value_details.invoice_discount_amt}",
+ "RndOffAmt": "{invoice_value_details.round_off}",
+ "OthChrg": "{invoice_value_details.total_other_charges}",
+ "TotInvVal": "{invoice_value_details.base_grand_total}",
+ "TotInvValFc": "{invoice_value_details.grand_total}"
+ }},
+ "PayDtls": {{
+ "Nm": "{payment_details.payee_name}",
+ "AccDet": "{payment_details.account_no}",
+ "Mode": "{payment_details.mode_of_payment}",
+ "FinInsBr": "{payment_details.ifsc_code}",
+ "PayTerm": "{payment_details.terms}",
+ "PaidAmt": "{payment_details.paid_amount}",
+ "PaymtDue": "{payment_details.outstanding_amount}"
+ }},
+ "RefDtls": {{
+ "DocPerdDtls": {{
+ "InvStDt": "{period_details.start_date}",
+ "InvEndDt": "{period_details.end_date}"
+ }},
+ "PrecDocDtls": [{{
+ "InvNo": "{prev_doc_details.invoice_name}",
+ "InvDt": "{prev_doc_details.invoice_date}"
+ }}]
+ }},
+ "ExpDtls": {{
+ "ShipBNo": "{export_details.bill_no}",
+ "ShipBDt": "{export_details.bill_date}",
+ "Port": "{export_details.port}",
+ "ForCur": "{export_details.foreign_curr_code}",
+ "CntCode": "{export_details.country_code}",
+ "ExpDuty": "{export_details.export_duty}"
+ }},
+ "EwbDtls": {{
+ "TransId": "{eway_bill_details.gstin}",
+ "TransName": "{eway_bill_details.name}",
+ "TransMode": "{eway_bill_details.mode_of_transport}",
+ "Distance": "{eway_bill_details.distance}",
+ "TransDocNo": "{eway_bill_details.document_name}",
+ "TransDocDt": "{eway_bill_details.document_date}",
+ "VehNo": "{eway_bill_details.vehicle_no}",
+ "VehType": "{eway_bill_details.vehicle_type}"
+ }}
+}}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json
new file mode 100644
index 0000000000..f4a3542a60
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -0,0 +1,957 @@
+{
+ "Version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6,
+ "description": "Version of the schema"
+ },
+ "Irn": {
+ "type": "string",
+ "minLength": 64,
+ "maxLength": 64,
+ "description": "Invoice Reference Number"
+ },
+ "TranDtls": {
+ "type": "object",
+ "properties": {
+ "TaxSch": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 10,
+ "enum": ["GST"],
+ "description": "GST- Goods and Services Tax Scheme"
+ },
+ "SupTyp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 10,
+ "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"],
+ "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export"
+ },
+ "RegRev": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Y- whether the tax liability is payable under reverse charge"
+ },
+ "EcmGstin": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "pattern": "([0-9]{2}[0-9A-Z]{13})",
+ "description": "E-Commerce GSTIN",
+ "validationMsg": "E-Commerce GSTIN is invalid"
+ },
+ "IgstOnIntra": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Y- indicates the supply is intra state but chargeable to IGST"
+ }
+ },
+ "required": ["TaxSch", "SupTyp"]
+ },
+ "DocDtls": {
+ "type": "object",
+ "properties": {
+ "Typ": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 3,
+ "enum": ["INV", "CRN", "DBN"],
+ "description": "Document Type"
+ },
+ "No": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
+ "description": "Document Number",
+ "validationMsg": "Document Number should not be starting with 0, / and -"
+ },
+ "Dt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Document Date"
+ }
+ },
+ "required": ["Typ", "No", "Dt"]
+ },
+ "SellerDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "pattern": "([0-9]{2}[0-9A-Z]{13})",
+ "description": "Supplier GSTIN",
+ "validationMsg": "Company GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Tradename"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Supplier State Code"
+ },
+ "Ph": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 12,
+ "description": "Phone"
+ },
+ "Em": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 100,
+ "description": "Email-Id"
+ }
+ },
+ "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "BuyerDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 15,
+ "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
+ "description": "Buyer GSTIN",
+ "validationMsg": "Customer GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Trade Name"
+ },
+ "Pos": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Place of Supply State code"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Buyer State Code"
+ },
+ "Ph": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 12,
+ "description": "Phone"
+ },
+ "Em": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 100,
+ "description": "Email-Id"
+ }
+ },
+ "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
+ },
+ "DispDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Dispatch Address Name"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "State Code"
+ }
+ },
+ "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "ShipDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "maxLength": 15,
+ "minLength": 3,
+ "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
+ "description": "Shipping Address GSTIN",
+ "validationMsg": "Shipping Address GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Trade Name"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "State Code"
+ }
+ },
+ "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "ItemList": {
+ "type": "Array",
+ "properties": {
+ "SlNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6,
+ "description": "Serial No. of Item"
+ },
+ "PrdDesc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "Item Name"
+ },
+ "IsServc": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Is Service Item"
+ },
+ "HsnCd": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 8,
+ "description": "HSN Code"
+ },
+ "Barcde": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 30,
+ "description": "Barcode"
+ },
+ "Qty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999999999.999,
+ "description": "Quantity"
+ },
+ "FreeQty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999999999.999,
+ "description": "Free Quantity"
+ },
+ "Unit": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 8,
+ "description": "UOM"
+ },
+ "UnitPrice": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.999,
+ "description": "Rate"
+ },
+ "TotAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Gross Amount"
+ },
+ "Discount": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Discount"
+ },
+ "PreTaxVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Pre tax value"
+ },
+ "AssAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Taxable Value"
+ },
+ "GstRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "GST Rate"
+ },
+ "IgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "IGST Amount"
+ },
+ "CgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "CGST Amount"
+ },
+ "SgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "SGST Amount"
+ },
+ "CesRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "Cess Rate"
+ },
+ "CesAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Cess Amount (Advalorem)"
+ },
+ "CesNonAdvlAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Cess Amount (Non-Advalorem)"
+ },
+ "StateCesRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "State CESS Rate"
+ },
+ "StateCesAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "State CESS Amount"
+ },
+ "StateCesNonAdvlAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "State CESS Amount (Non Advalorem)"
+ },
+ "OthChrg": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Other Charges"
+ },
+ "TotItemVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Total Item Value"
+ },
+ "OrdLineRef": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "description": "Order line reference"
+ },
+ "OrgCntry": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 2,
+ "description": "Origin Country"
+ },
+ "PrdSlNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Serial number"
+ },
+ "BchDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 20,
+ "description": "Batch number"
+ },
+ "ExpDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Batch Expiry Date"
+ },
+ "WrDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Warranty Date"
+ }
+ },
+ "required": ["Nm"]
+ },
+ "AttribDtls": {
+ "type": "Array",
+ "Attribute": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Attribute name of the item"
+ },
+ "Val": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Attribute value of the item"
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "SlNo",
+ "IsServc",
+ "HsnCd",
+ "UnitPrice",
+ "TotAmt",
+ "AssAmt",
+ "GstRt",
+ "TotItemVal"
+ ]
+ },
+ "ValDtls": {
+ "type": "object",
+ "properties": {
+ "AssVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total Assessable value of all items"
+ },
+ "CgstVal": {
+ "type": "number",
+ "maximum": 99999999999999.99,
+ "minimum": 0,
+ "description": "Total CGST value of all items"
+ },
+ "SgstVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total SGST value of all items"
+ },
+ "IgstVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total IGST value of all items"
+ },
+ "CesVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total CESS value of all items"
+ },
+ "StCesVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total State CESS value of all items"
+ },
+ "Discount": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Invoice Discount"
+ },
+ "OthChrg": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Other Charges"
+ },
+ "RndOffAmt": {
+ "type": "number",
+ "minimum": -99.99,
+ "maximum": 99.99,
+ "description": "Rounded off Amount"
+ },
+ "TotInvVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Final Invoice Value "
+ },
+ "TotInvValFc": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Final Invoice value in Foreign Currency"
+ }
+ },
+ "required": ["AssVal", "TotInvVal"]
+ },
+ "PayDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Payee Name"
+ },
+ "AccDet": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 18,
+ "description": "Bank Account Number of Payee"
+ },
+ "Mode": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 18,
+ "description": "Mode of Payment"
+ },
+ "FinInsBr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 11,
+ "description": "Branch or IFSC code"
+ },
+ "PayTerm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Terms of Payment"
+ },
+ "PayInstr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Payment Instruction"
+ },
+ "CrTrn": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Credit Transfer"
+ },
+ "DirDr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Direct Debit"
+ },
+ "CrDay": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999,
+ "description": "Credit Days"
+ },
+ "PaidAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Advance Amount"
+ },
+ "PaymtDue": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Outstanding Amount"
+ }
+ }
+ },
+ "RefDtls": {
+ "type": "object",
+ "properties": {
+ "InvRm": {
+ "type": "string",
+ "maxLength": 100,
+ "minLength": 3,
+ "pattern": "^[0-9A-Za-z/-]{3,100}$",
+ "description": "Remarks/Note"
+ },
+ "DocPerdDtls": {
+ "type": "object",
+ "properties": {
+ "InvStDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Invoice Period Start Date"
+ },
+ "InvEndDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Invoice Period End Date"
+ }
+ },
+ "required": ["InvStDt ", "InvEndDt "]
+ },
+ "PrecDocDtls": {
+ "type": "object",
+ "properties": {
+ "InvNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$",
+ "description": "Reference of Original Invoice"
+ },
+ "InvDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Date of Orginal Invoice"
+ },
+ "OthRefNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Other Reference"
+ }
+ }
+ },
+ "required": ["InvNo", "InvDt"],
+ "ContrDtls": {
+ "type": "object",
+ "properties": {
+ "RecAdvRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Receipt Advice No."
+ },
+ "RecAdvDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Date of receipt advice"
+ },
+ "TendRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Lot/Batch Reference No."
+ },
+ "ContrRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Contract Reference Number"
+ },
+ "ExtRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Any other reference"
+ },
+ "ProjRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Project Reference Number"
+ },
+ "PORefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^([0-9A-Za-z/-]){1,16}$",
+ "description": "PO Reference Number"
+ },
+ "PORefDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "PO Reference date"
+ }
+ }
+ }
+ }
+ },
+ "AddlDocDtls": {
+ "type": "Array",
+ "properties": {
+ "Url": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Supporting document URL"
+ },
+ "Docs": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 1000,
+ "description": "Supporting document in Base64 Format"
+ },
+ "Info": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 1000,
+ "description": "Any additional information"
+ }
+ }
+ },
+
+ "ExpDtls": {
+ "type": "object",
+ "properties": {
+ "ShipBNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Shipping Bill No."
+ },
+ "ShipBDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Shipping Bill Date"
+ },
+ "Port": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 10,
+ "pattern": "^[0-9A-Za-z]{2,10}$",
+ "description": "Port Code. Refer the master"
+ },
+ "RefClm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "description": "Claiming Refund. Y/N"
+ },
+ "ForCur": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 16,
+ "description": "Additional Currency Code. Refer the master"
+ },
+ "CntCode": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 2,
+ "description": "Country Code. Refer the master"
+ },
+ "ExpDuty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Export Duty"
+ }
+ }
+ },
+ "EwbDtls": {
+ "type": "object",
+ "properties": {
+ "TransId": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "description": "Transporter GSTIN"
+ },
+ "TransName": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Transporter Name"
+ },
+ "TransMode": {
+ "type": "string",
+ "maxLength": 1,
+ "minLength": 1,
+ "enum": ["1", "2", "3", "4"],
+ "description": "Mode of Transport"
+ },
+ "Distance": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 9999,
+ "description": "Distance"
+ },
+ "TransDocNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 15,
+ "pattern": "^([0-9A-Z/-]){1,15}$",
+ "description": "Tranport Document Number",
+ "validationMsg": "Transport Receipt No is invalid"
+ },
+ "TransDocDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Transport Document Date"
+ },
+ "VehNo": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 20,
+ "description": "Vehicle Number"
+ },
+ "VehType": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["O", "R"],
+ "description": "Vehicle Type"
+ }
+ },
+ "required": ["Distance"]
+ },
+ "required": [
+ "Version",
+ "TranDtls",
+ "DocDtls",
+ "SellerDtls",
+ "BuyerDtls",
+ "ItemList",
+ "ValDtls"
+ ]
+}
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
new file mode 100644
index 0000000000..348f0c6fee
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -0,0 +1,292 @@
+erpnext.setup_einvoice_actions = (doctype) => {
+ frappe.ui.form.on(doctype, {
+ async refresh(frm) {
+ if (frm.doc.docstatus == 2) return;
+
+ const res = await frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
+ args: { doc: frm.doc }
+ });
+ const invoice_eligible = res.message;
+
+ if (!invoice_eligible) return;
+
+ const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
+
+ const add_custom_button = (label, action) => {
+ if (!frm.custom_buttons[label]) {
+ frm.add_custom_button(label, action, __('E Invoicing'));
+ }
+ };
+
+ if (!irn && !__unsaved) {
+ const action = () => {
+ if (frm.doc.__unsaved) {
+ frappe.throw(__('Please save the document to generate IRN.'));
+ }
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: (res) => {
+ const einvoice = res.message;
+ show_einvoice_preview(frm, einvoice);
+ }
+ });
+ };
+
+ add_custom_button(__("Generate IRN"), action);
+ }
+
+ if (irn && !irn_cancelled && !ewaybill) {
+ const fields = [
+ {
+ "label": "Reason",
+ "fieldname": "reason",
+ "fieldtype": "Select",
+ "reqd": 1,
+ "default": "1-Duplicate",
+ "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
+ },
+ {
+ "label": "Remark",
+ "fieldname": "remark",
+ "fieldtype": "Data",
+ "reqd": 1
+ }
+ ];
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __("Cancel IRN"),
+ fields: fields,
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
+ args: {
+ doctype,
+ docname: name,
+ irn: irn,
+ reason: data.reason.split('-')[0],
+ remark: data.remark
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+ add_custom_button(__("Cancel IRN"), action);
+ }
+
+ if (irn && !irn_cancelled && !ewaybill) {
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __('Generate E-Way Bill'),
+ size: "large",
+ fields: get_ewaybill_fields(frm),
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill',
+ args: {
+ doctype,
+ docname: name,
+ irn,
+ ...data
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+
+ add_custom_button(__("Generate E-Way Bill"), action);
+ }
+
+ if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
+ const action = () => {
+ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
+ message += '
';
+ message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
+
+ const dialog = frappe.msgprint({
+ title: __('Update E-Way Bill Cancelled Status?'),
+ message: message,
+ indicator: 'orange',
+ primary_action: {
+ action: function() {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: () => frm.reload_doc() || dialog.hide()
+ });
+ }
+ },
+ primary_action_label: __('Yes')
+ });
+ };
+ add_custom_button(__("Cancel E-Way Bill"), action);
+ }
+ }
+ });
+};
+
+const get_ewaybill_fields = (frm) => {
+ return [
+ {
+ 'fieldname': 'transporter',
+ 'label': 'Transporter',
+ 'fieldtype': 'Link',
+ 'options': 'Supplier',
+ 'default': frm.doc.transporter
+ },
+ {
+ 'fieldname': 'gst_transporter_id',
+ 'label': 'GST Transporter ID',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'transporter.gst_transporter_id',
+ 'default': frm.doc.gst_transporter_id
+ },
+ {
+ 'fieldname': 'driver',
+ 'label': 'Driver',
+ 'fieldtype': 'Link',
+ 'options': 'Driver',
+ 'default': frm.doc.driver
+ },
+ {
+ 'fieldname': 'lr_no',
+ 'label': 'Transport Receipt No',
+ 'fieldtype': 'Data',
+ 'default': frm.doc.lr_no
+ },
+ {
+ 'fieldname': 'vehicle_no',
+ 'label': 'Vehicle No',
+ 'fieldtype': 'Data',
+ 'default': frm.doc.vehicle_no
+ },
+ {
+ 'fieldname': 'distance',
+ 'label': 'Distance (in km)',
+ 'fieldtype': 'Float',
+ 'default': frm.doc.distance
+ },
+ {
+ 'fieldname': 'transporter_col_break',
+ 'fieldtype': 'Column Break',
+ },
+ {
+ 'fieldname': 'transporter_name',
+ 'label': 'Transporter Name',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'transporter.name',
+ 'read_only': 1,
+ 'default': frm.doc.transporter_name
+ },
+ {
+ 'fieldname': 'mode_of_transport',
+ 'label': 'Mode of Transport',
+ 'fieldtype': 'Select',
+ 'options': `\nRoad\nAir\nRail\nShip`,
+ 'default': frm.doc.mode_of_transport
+ },
+ {
+ 'fieldname': 'driver_name',
+ 'label': 'Driver Name',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'driver.full_name',
+ 'read_only': 1,
+ 'default': frm.doc.driver_name
+ },
+ {
+ 'fieldname': 'lr_date',
+ 'label': 'Transport Receipt Date',
+ 'fieldtype': 'Date',
+ 'default': frm.doc.lr_date
+ },
+ {
+ 'fieldname': 'gst_vehicle_type',
+ 'label': 'GST Vehicle Type',
+ 'fieldtype': 'Select',
+ 'options': `Regular\nOver Dimensional Cargo (ODC)`,
+ 'depends_on': 'eval:(doc.mode_of_transport === "Road")',
+ 'default': frm.doc.gst_vehicle_type
+ }
+ ];
+};
+
+const request_irn_generation = (frm) => {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_irn',
+ args: { doctype: frm.doc.doctype, docname: frm.doc.name },
+ freeze: true,
+ callback: () => frm.reload_doc()
+ });
+};
+
+const get_preview_dialog = (frm, action) => {
+ const dialog = new frappe.ui.Dialog({
+ title: __("Preview"),
+ size: "large",
+ fields: [
+ {
+ "label": "Preview",
+ "fieldname": "preview_html",
+ "fieldtype": "HTML"
+ }
+ ],
+ primary_action: () => action(frm) || dialog.hide(),
+ primary_action_label: __('Generate IRN')
+ });
+ return dialog;
+};
+
+const show_einvoice_preview = (frm, einvoice) => {
+ const preview_dialog = get_preview_dialog(frm, request_irn_generation);
+
+ // initialize e-invoice fields
+ einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate();
+ frm.doc.signed_einvoice = JSON.stringify(einvoice);
+
+ // initialize preview wrapper
+ const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper;
+ $preview_wrapper.html(
+ `
`
+ );
+
+ frappe.call({
+ method: "frappe.www.printview.get_html_and_style",
+ args: {
+ doc: frm.doc,
+ print_format: "GST E-Invoice",
+ no_letterhead: 1
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ $preview_wrapper.find(".print-format").html(r.message.html);
+ const style = `
+ .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; }
+ .print-preview { min-height: 0px; }
+ .modal-dialog { width: 720px; }`;
+
+ frappe.dom.set_style(style, "custom-print-style");
+ preview_dialog.show();
+ }
+ }
+ });
+};
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
new file mode 100644
index 0000000000..e3f7e90ff3
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -0,0 +1,1171 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import base64
+import io
+import json
+import os
+import re
+import sys
+import traceback
+
+import frappe
+import jwt
+from frappe import _, bold
+from frappe.core.page.background_jobs.background_jobs import get_info
+from frappe.integrations.utils import make_get_request, make_post_request
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.data import (
+ add_to_date,
+ cint,
+ cstr,
+ flt,
+ format_date,
+ get_link_to_form,
+ getdate,
+ now_datetime,
+ time_diff_in_hours,
+ time_diff_in_seconds,
+)
+from frappe.utils.scheduler import is_scheduler_inactive
+from pyqrcode import create as qrcreate
+
+from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
+
+
+@frappe.whitelist()
+def validate_eligibility(doc):
+ if isinstance(doc, str):
+ doc = json.loads(doc)
+
+ invalid_doctype = doc.get('doctype') != 'Sales Invoice'
+ if invalid_doctype:
+ return False
+
+ einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable'))
+ if not einvoicing_enabled:
+ return False
+
+ einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01'
+ if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from):
+ return False
+
+ invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
+ invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
+ company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
+
+ # if export invoice, then taxes can be empty
+ # invoice can only be ineligible if no taxes applied and is not an export invoice
+ no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
+ has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
+
+ if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
+ return False
+
+ return True
+
+def validate_einvoice_fields(doc):
+ invoice_eligible = validate_eligibility(doc)
+
+ if not invoice_eligible:
+ return
+
+ if doc.docstatus == 0 and doc._action == 'save':
+ if doc.irn:
+ frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed'))
+ if len(doc.name) > 16:
+ raise_document_name_too_long_error()
+
+ doc.einvoice_status = 'Pending'
+
+ elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
+ frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
+
+ elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
+ frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
+
+def raise_document_name_too_long_error():
+ title = _('Document ID Too Long')
+ msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice')
+ msg += ', '
+ msg += _('document id {} exceed 16 letters.').format(bold(_('should not')))
+ msg += '
'
+ msg += _('You must {} your {} in order to have document id of {} length 16.').format(
+ bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
+ )
+ msg += _('Please account for ammended documents too.')
+ frappe.throw(msg, title=title)
+
+def read_json(name):
+ file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
+ with open(file_path, 'r') as f:
+ return cstr(f.read())
+
+def get_transaction_details(invoice):
+ supply_type = ''
+ if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
+ elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
+ elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
+ elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
+
+ if not supply_type:
+ rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
+ frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
+ title=_('Invalid Supply Type'))
+
+ return frappe._dict(dict(
+ tax_scheme='GST',
+ supply_type=supply_type,
+ reverse_charge=invoice.reverse_charge
+ ))
+
+def get_doc_details(invoice):
+ if getdate(invoice.posting_date) < getdate('2021-01-01'):
+ frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed'))
+
+ invoice_type = 'CRN' if invoice.is_return else 'INV'
+
+ invoice_name = invoice.name
+ invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
+
+ return frappe._dict(dict(
+ invoice_type=invoice_type,
+ invoice_name=invoice_name,
+ invoice_date=invoice_date
+ ))
+
+def validate_address_fields(address, skip_gstin_validation):
+ if ((not address.gstin and not skip_gstin_validation)
+ or not address.city
+ or not address.pincode
+ or not address.address_title
+ or not address.address_line1
+ or not address.gst_state_number):
+
+ frappe.throw(
+ msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
+ title=_('Missing Address Fields')
+ )
+
+ if address.address_line2 and len(address.address_line2) < 2:
+ # to prevent "The field Address 2 must be a string with a minimum length of 3 and a maximum length of 100"
+ address.address_line2 = ""
+
+def get_party_details(address_name, skip_gstin_validation=False):
+ addr = frappe.get_doc('Address', address_name)
+
+ validate_address_fields(addr, skip_gstin_validation)
+
+ if addr.gst_state_number == 97:
+ # according to einvoice standard
+ addr.pincode = 999999
+
+ party_address_details = frappe._dict(dict(
+ legal_name=sanitize_for_json(addr.address_title),
+ location=sanitize_for_json(addr.city),
+ pincode=addr.pincode, gstin=addr.gstin,
+ state_code=addr.gst_state_number,
+ address_line1=sanitize_for_json(addr.address_line1),
+ address_line2=sanitize_for_json(addr.address_line2)
+ ))
+
+ return party_address_details
+
+def get_overseas_address_details(address_name):
+ address_title, address_line1, address_line2, city = frappe.db.get_value(
+ 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
+ )
+
+ if not address_title or not address_line1 or not city:
+ frappe.throw(
+ msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
+ get_link_to_form('Address', address_name)
+ ),
+ title=_('Missing Address Fields')
+ )
+
+ return frappe._dict(dict(
+ gstin='URP',
+ legal_name=sanitize_for_json(address_title),
+ location=city,
+ address_line1=sanitize_for_json(address_line1),
+ address_line2=sanitize_for_json(address_line2),
+ pincode=999999, state_code=96, place_of_supply=96
+ ))
+
+def get_item_list(invoice):
+ item_list = []
+
+ for d in invoice.items:
+ einvoice_item_schema = read_json('einv_item_template')
+ item = frappe._dict({})
+ item.update(d.as_dict())
+
+ item.sr_no = d.idx
+ item.description = sanitize_for_json(d.item_name)
+
+ item.qty = abs(item.qty)
+ if flt(item.qty) != 0.0:
+ item.unit_rate = abs(item.taxable_value / item.qty)
+ else:
+ item.unit_rate = abs(item.taxable_value)
+ item.gross_amount = abs(item.taxable_value)
+ item.taxable_value = abs(item.taxable_value)
+ item.discount_amount = 0
+
+ item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
+ item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
+ item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
+ item.serial_no = ""
+
+ item = update_item_taxes(invoice, item)
+
+ item.total_value = abs(
+ item.taxable_value + item.igst_amount + item.sgst_amount +
+ item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
+ )
+ einv_item = einvoice_item_schema.format(item=item)
+ item_list.append(einv_item)
+
+ return ', '.join(item_list)
+
+def update_item_taxes(invoice, item):
+ gst_accounts = get_gst_accounts(invoice.company)
+ gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
+
+ for attr in [
+ 'tax_rate', 'cess_rate', 'cess_nadv_amount',
+ 'cgst_amount', 'sgst_amount', 'igst_amount',
+ 'cess_amount', 'cess_nadv_amount', 'other_charges'
+ ]:
+ item[attr] = 0
+
+ for t in invoice.taxes:
+ is_applicable = t.tax_amount and t.account_head in gst_accounts_list
+ if is_applicable:
+ # this contains item wise tax rate & tax amount (incl. discount)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
+
+ item_tax_rate = item_tax_detail[0]
+ # item tax amount excluding discount amount
+ item_tax_amount = (item_tax_rate / 100) * item.taxable_value
+
+ if t.account_head in gst_accounts.cess_account:
+ item_tax_amount_after_discount = item_tax_detail[1]
+ if t.charge_type == 'On Item Quantity':
+ item.cess_nadv_amount += abs(item_tax_amount_after_discount)
+ else:
+ item.cess_rate += item_tax_rate
+ item.cess_amount += abs(item_tax_amount_after_discount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts[f'{tax_type}_account']:
+ item.tax_rate += item_tax_rate
+ item[f'{tax_type}_amount'] += abs(item_tax_amount)
+ else:
+ # TODO: other charges per item
+ pass
+
+ return item
+
+def get_invoice_value_details(invoice):
+ invoice_value_details = frappe._dict(dict())
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
+ invoice_value_details.invoice_discount_amt = 0
+
+ invoice_value_details.round_off = invoice.base_rounding_adjustment
+ invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
+ invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
+
+ invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
+
+ return invoice_value_details
+
+def update_invoice_taxes(invoice, invoice_value_details):
+ gst_accounts = get_gst_accounts(invoice.company)
+ gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
+
+ invoice_value_details.total_cgst_amt = 0
+ invoice_value_details.total_sgst_amt = 0
+ invoice_value_details.total_igst_amt = 0
+ invoice_value_details.total_cess_amt = 0
+ invoice_value_details.total_other_charges = 0
+ considered_rows = []
+
+ for t in invoice.taxes:
+ tax_amount = t.base_tax_amount_after_discount_amount
+ if t.account_head in gst_accounts_list:
+ if t.account_head in gst_accounts.cess_account:
+ # using after discount amt since item also uses after discount amt for cess calc
+ invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts[f'{tax_type}_account']:
+
+ invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
+ update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
+ else:
+ invoice_value_details.total_other_charges += abs(tax_amount)
+
+ return invoice_value_details
+
+def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
+ prev_row_id = cint(tax_row.row_id) - 1
+ if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
+ if tax_row.charge_type == 'On Previous Row Amount':
+ amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+ if tax_row.charge_type == 'On Previous Row Total':
+ amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+
+def get_payment_details(invoice):
+ payee_name = invoice.company
+ mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
+ paid_amount = invoice.base_paid_amount
+ outstanding_amount = invoice.outstanding_amount
+
+ return frappe._dict(dict(
+ payee_name=payee_name, mode_of_payment=mode_of_payment,
+ paid_amount=paid_amount, outstanding_amount=outstanding_amount
+ ))
+
+def get_return_doc_reference(invoice):
+ invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
+ return frappe._dict(dict(
+ invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
+ ))
+
+def get_eway_bill_details(invoice):
+ if invoice.is_return:
+ frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
+ title=_('Invalid Fields'))
+
+
+ mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
+ vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
+
+ return frappe._dict(dict(
+ gstin=invoice.gst_transporter_id,
+ name=invoice.transporter_name,
+ mode_of_transport=mode_of_transport[invoice.mode_of_transport],
+ distance=invoice.distance or 0,
+ document_name=invoice.lr_no,
+ document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
+ vehicle_no=invoice.vehicle_no,
+ vehicle_type=vehicle_type[invoice.gst_vehicle_type]
+ ))
+
+def validate_mandatory_fields(invoice):
+ if not invoice.company_address:
+ frappe.throw(
+ _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
+ if not invoice.customer_address:
+ frappe.throw(
+ _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
+ if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
+ title=_('Missing Fields')
+ )
+ if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
+ title=_('Missing Fields')
+ )
+
+def validate_totals(einvoice):
+ item_list = einvoice['ItemList']
+ value_details = einvoice['ValDtls']
+
+ total_item_ass_value = 0
+ total_item_cgst_value = 0
+ total_item_sgst_value = 0
+ total_item_igst_value = 0
+ total_item_value = 0
+ for item in item_list:
+ total_item_ass_value += flt(item['AssAmt'])
+ total_item_cgst_value += flt(item['CgstAmt'])
+ total_item_sgst_value += flt(item['SgstAmt'])
+ total_item_igst_value += flt(item['IgstAmt'])
+ total_item_value += flt(item['TotItemVal'])
+
+ if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
+ frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
+
+ if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
+ frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
+
+ if abs(flt(value_details['CgstVal']) + flt(value_details['SgstVal']) - total_item_cgst_value - total_item_sgst_value) > 1:
+ frappe.throw(_('CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction.'))
+
+ if abs(flt(value_details['IgstVal']) - total_item_igst_value) > 1:
+ frappe.throw(_('IGST value of all items is not equal to total IGST value. Please review taxes for any correction.'))
+
+ if abs(
+ flt(value_details['TotInvVal']) + flt(value_details['Discount']) -
+ flt(value_details['OthChrg']) - flt(value_details['RndOffAmt']) -
+ total_item_value
+ ) > 1:
+ frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
+
+ calculated_invoice_value = \
+ flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
+ + flt(value_details['OthChrg']) + flt(value_details['RndOffAmt']) - flt(value_details['Discount'])
+
+ if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
+ frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
+
+def make_einvoice(invoice):
+ validate_mandatory_fields(invoice)
+
+ schema = read_json('einv_template')
+
+ transaction_details = get_transaction_details(invoice)
+ item_list = get_item_list(invoice)
+ doc_details = get_doc_details(invoice)
+ invoice_value_details = get_invoice_value_details(invoice)
+ seller_details = get_party_details(invoice.company_address)
+
+ if invoice.gst_category == 'Overseas':
+ buyer_details = get_overseas_address_details(invoice.customer_address)
+ else:
+ buyer_details = get_party_details(invoice.customer_address)
+ place_of_supply = get_place_of_supply(invoice, invoice.doctype)
+ if place_of_supply:
+ place_of_supply = place_of_supply.split('-')[0]
+ else:
+ place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
+ buyer_details.update(dict(place_of_supply=place_of_supply))
+
+ seller_details.update(dict(legal_name=invoice.company))
+ buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
+
+ shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
+ if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
+ if invoice.gst_category == 'Overseas':
+ shipping_details = get_overseas_address_details(invoice.shipping_address_name)
+ else:
+ shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True)
+
+ dispatch_details = frappe._dict({})
+ if invoice.dispatch_address_name:
+ dispatch_details = get_party_details(invoice.dispatch_address_name, skip_gstin_validation=True)
+
+ if invoice.is_pos and invoice.base_paid_amount:
+ payment_details = get_payment_details(invoice)
+
+ if invoice.is_return and invoice.return_against:
+ prev_doc_details = get_return_doc_reference(invoice)
+
+ if invoice.transporter and not invoice.is_return:
+ eway_bill_details = get_eway_bill_details(invoice)
+
+ # not yet implemented
+ period_details = export_details = frappe._dict({})
+
+ einvoice = schema.format(
+ transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
+ seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
+ item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details,
+ period_details=period_details, prev_doc_details=prev_doc_details,
+ export_details=export_details, eway_bill_details=eway_bill_details
+ )
+
+ try:
+ einvoice = safe_json_load(einvoice)
+ einvoice = santize_einvoice_fields(einvoice)
+ except Exception:
+ show_link_to_error_log(invoice, einvoice)
+
+ try:
+ validate_totals(einvoice)
+ except Exception:
+ log_error(einvoice)
+ raise
+
+ return einvoice
+
+def show_link_to_error_log(invoice, einvoice):
+ err_log = log_error(einvoice)
+ link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
+ frappe.throw(
+ _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
+ invoice.name, link_to_error_log),
+ title=_('E Invoice Creation Failed')
+ )
+
+def log_error(data=None):
+ if isinstance(data, str):
+ data = json.loads(data)
+
+ seperator = "--" * 50
+ err_tb = traceback.format_exc()
+ err_msg = str(sys.exc_info()[1])
+ data = json.dumps(data, indent=4)
+
+ message = "\n".join([
+ "Error", err_msg, seperator,
+ "Data:", data, seperator,
+ "Exception:", err_tb
+ ])
+ return frappe.log_error(title=_('E Invoice Request Failed'), message=message)
+
+def santize_einvoice_fields(einvoice):
+ int_fields = ["Pin","Distance","CrDay"]
+ float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
+ copy = einvoice.copy()
+ for key, value in copy.items():
+ if isinstance(value, list):
+ for idx, d in enumerate(value):
+ santized_dict = santize_einvoice_fields(d)
+ if santized_dict:
+ einvoice[key][idx] = santized_dict
+ else:
+ einvoice[key].pop(idx)
+
+ if not einvoice[key]:
+ einvoice.pop(key, None)
+
+ elif isinstance(value, dict):
+ santized_dict = santize_einvoice_fields(value)
+ if santized_dict:
+ einvoice[key] = santized_dict
+ else:
+ einvoice.pop(key, None)
+
+ elif not value or value == "None":
+ einvoice.pop(key, None)
+
+ elif key in float_fields:
+ einvoice[key] = flt(value, 2)
+
+ elif key in int_fields:
+ einvoice[key] = cint(value)
+
+ return einvoice
+
+def safe_json_load(json_string):
+ try:
+ return json.loads(json_string)
+ except json.JSONDecodeError as e:
+ # print a snippet of 40 characters around the location where error occured
+ pos = e.pos
+ start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
+ snippet = json_string[start:end]
+ frappe.throw(_("Error in input data. Please check for any special characters near following input:
{}").format(snippet))
+
+class RequestFailed(Exception):
+ pass
+class CancellationNotAllowed(Exception):
+ pass
+
+class GSPConnector():
+ def __init__(self, doctype=None, docname=None):
+ self.doctype = doctype
+ self.docname = docname
+
+ self.set_invoice()
+ self.set_credentials()
+
+ # authenticate url is same for sandbox & live
+ self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
+ self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test'
+
+ self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
+ self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
+ self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
+ self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
+ self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
+
+ def set_invoice(self):
+ self.invoice = None
+ if self.doctype and self.docname:
+ self.invoice = frappe.get_cached_doc(self.doctype, self.docname)
+
+ def set_credentials(self):
+ self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+
+ if not self.e_invoice_settings.enable:
+ frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
+
+ if self.invoice:
+ gstin = self.get_seller_gstin()
+ credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
+ if credentials_for_gstin:
+ self.credentials = credentials_for_gstin[0]
+ else:
+ frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
+ else:
+ self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
+
+ def get_seller_gstin(self):
+ gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
+ if not gstin:
+ frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
+ return gstin
+
+ def get_auth_token(self):
+ if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0:
+ self.fetch_auth_token()
+
+ return self.e_invoice_settings.auth_token
+
+ def make_request(self, request_type, url, headers=None, data=None):
+ if request_type == 'post':
+ res = make_post_request(url, headers=headers, data=data)
+ else:
+ res = make_get_request(url, headers=headers, data=data)
+
+ self.log_request(url, headers, data, res)
+ return res
+
+ def log_request(self, url, headers, data, res):
+ headers.update({ 'password': self.credentials.password })
+ request_log = frappe.get_doc({
+ "doctype": "E Invoice Request Log",
+ "user": frappe.session.user,
+ "reference_invoice": self.invoice.name if self.invoice else None,
+ "url": url,
+ "headers": json.dumps(headers, indent=4) if headers else None,
+ "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
+ "response": json.dumps(res, indent=4) if res else None
+ })
+ request_log.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ def get_client_credentials(self):
+ if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret:
+ return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret')
+
+ return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret
+
+ def fetch_auth_token(self):
+ client_id, client_secret = self.get_client_credentials()
+ headers = {
+ 'gspappid': client_id,
+ 'gspappsecret': client_secret
+ }
+ res = {}
+ try:
+ res = self.make_request('post', self.authenticate_url, headers)
+ self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
+ self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
+ self.e_invoice_settings.save(ignore_permissions=True)
+ self.e_invoice_settings.reload()
+
+ except Exception:
+ log_error(res)
+ self.raise_error(True)
+
+ def get_headers(self):
+ return {
+ 'content-type': 'application/json',
+ 'user_name': self.credentials.username,
+ 'password': self.credentials.get_password(),
+ 'gstin': self.credentials.gstin,
+ 'authorization': self.get_auth_token(),
+ 'requestid': str(base64.b64encode(os.urandom(18))),
+ }
+
+ def fetch_gstin_details(self, gstin):
+ headers = self.get_headers()
+
+ try:
+ params = '?gstin={gstin}'.format(gstin=gstin)
+ res = self.make_request('get', self.gstin_details_url + params, headers)
+ if res.get('success'):
+ return res.get('result')
+ else:
+ log_error(res)
+ raise RequestFailed
+
+ except RequestFailed:
+ self.raise_error()
+
+ except Exception:
+ log_error()
+ self.raise_error(True)
+ @staticmethod
+ def get_gstin_details(gstin):
+ '''fetch and cache GSTIN details'''
+ if not hasattr(frappe.local, 'gstin_cache'):
+ frappe.local.gstin_cache = {}
+
+ key = gstin
+ gsp_connector = GSPConnector()
+ details = gsp_connector.fetch_gstin_details(gstin)
+
+ frappe.local.gstin_cache[key] = details
+ frappe.cache().hset('gstin_cache', key, details)
+ return details
+
+ def generate_irn(self):
+ data = {}
+ try:
+ headers = self.get_headers()
+ einvoice = make_einvoice(self.invoice)
+ data = json.dumps(einvoice, indent=4)
+ res = self.make_request('post', self.generate_irn_url, headers, data)
+
+ if res.get('success'):
+ self.set_einvoice_data(res.get('result'))
+
+ elif '2150' in res.get('message'):
+ # IRN already generated but not updated in invoice
+ # Extract the IRN from the response description and fetch irn details
+ irn = res.get('result')[0].get('Desc').get('Irn')
+ irn_details = self.get_irn_details(irn)
+ if irn_details:
+ self.set_einvoice_data(irn_details)
+ else:
+ raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
+ Contact ERPNext support to resolve the issue.')
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.set_failed_status(errors=errors)
+ self.raise_error(errors=errors)
+
+ except Exception as e:
+ self.set_failed_status(errors=str(e))
+ log_error(data)
+ self.raise_error(True)
+
+ @staticmethod
+ def bulk_generate_irn(invoices):
+ gsp_connector = GSPConnector()
+ gsp_connector.doctype = 'Sales Invoice'
+
+ failed = []
+
+ for invoice in invoices:
+ try:
+ gsp_connector.docname = invoice
+ gsp_connector.set_invoice()
+ gsp_connector.set_credentials()
+ gsp_connector.generate_irn()
+
+ except Exception as e:
+ failed.append({
+ 'docname': invoice,
+ 'message': str(e)
+ })
+
+ return failed
+
+ def get_irn_details(self, irn):
+ headers = self.get_headers()
+
+ try:
+ params = '?irn={irn}'.format(irn=irn)
+ res = self.make_request('get', self.irn_details_url + params, headers)
+ if res.get('success'):
+ return res.get('result')
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ log_error()
+ self.raise_error(True)
+
+ def cancel_irn(self, irn, reason, remark):
+ data, res = {}, {}
+ try:
+ # validate cancellation
+ if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24:
+ frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+ if not irn:
+ frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+
+ headers = self.get_headers()
+ data = json.dumps({
+ 'Irn': irn,
+ 'Cnlrsn': reason,
+ 'Cnlrem': remark
+ }, indent=4)
+
+ res = self.make_request('post', self.cancel_irn_url, headers, data)
+ if res.get('success') or '9999' in res.get('message'):
+ self.invoice.irn_cancelled = 1
+ self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else ""
+ self.invoice.einvoice_status = 'Cancelled'
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('IRN Cancelled - {}').format(remark)
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.set_failed_status(errors=errors)
+ self.raise_error(errors=errors)
+
+ except CancellationNotAllowed as e:
+ self.set_failed_status(errors=str(e))
+ self.raise_error(errors=str(e))
+
+ except Exception as e:
+ self.set_failed_status(errors=str(e))
+ log_error(data)
+ self.raise_error(True)
+
+ @staticmethod
+ def bulk_cancel_irn(invoices, reason, remark):
+ gsp_connector = GSPConnector()
+ gsp_connector.doctype = 'Sales Invoice'
+
+ failed = []
+
+ for invoice in invoices:
+ try:
+ gsp_connector.docname = invoice
+ gsp_connector.set_invoice()
+ gsp_connector.set_credentials()
+ irn = gsp_connector.invoice.irn
+ gsp_connector.cancel_irn(irn, reason, remark)
+
+ except Exception as e:
+ failed.append({
+ 'docname': invoice,
+ 'message': str(e)
+ })
+
+ return failed
+
+ def generate_eway_bill(self, **kwargs):
+ args = frappe._dict(kwargs)
+
+ headers = self.get_headers()
+ eway_bill_details = get_eway_bill_details(args)
+ data = json.dumps({
+ 'Irn': args.irn,
+ 'Distance': cint(eway_bill_details.distance),
+ 'TransMode': eway_bill_details.mode_of_transport,
+ 'TransId': eway_bill_details.gstin,
+ 'TransName': eway_bill_details.transporter,
+ 'TrnDocDt': eway_bill_details.document_date,
+ 'TrnDocNo': eway_bill_details.document_name,
+ 'VehNo': eway_bill_details.vehicle_no,
+ 'VehType': eway_bill_details.vehicle_type
+ }, indent=4)
+
+ try:
+ res = self.make_request('post', self.generate_ewaybill_url, headers, data)
+ if res.get('success'):
+ self.invoice.ewaybill = res.get('result').get('EwbNo')
+ self.invoice.eway_bill_validity = res.get('result').get('EwbValidTill')
+ self.invoice.eway_bill_cancelled = 0
+ self.invoice.update(args)
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('E-Way Bill Generated')
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ log_error(data)
+ self.raise_error(True)
+
+ def cancel_eway_bill(self, eway_bill, reason, remark):
+ headers = self.get_headers()
+ data = json.dumps({
+ 'ewbNo': eway_bill,
+ 'cancelRsnCode': reason,
+ 'cancelRmrk': remark
+ }, indent=4)
+ headers["username"] = headers["user_name"]
+ del headers["user_name"]
+ try:
+ res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
+ if res.get('success'):
+ self.invoice.ewaybill = ''
+ self.invoice.eway_bill_cancelled = 1
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('E-Way Bill Cancelled - {}').format(remark)
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ log_error(data)
+ self.raise_error(True)
+
+ def sanitize_error_message(self, message):
+ '''
+ On validation errors, response message looks something like this:
+ message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
+ 3095 : Supplier GSTIN is inactive'
+ we search for string between ':' to extract the error messages
+ errors = [
+ ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
+ ': Test'
+ ]
+ then we trim down the message by looping over errors
+ '''
+ if not message:
+ return []
+
+ errors = re.findall(': [^:]+', message)
+ for idx, e in enumerate(errors):
+ # remove colons
+ errors[idx] = errors[idx].replace(':', '').strip()
+ # if not last
+ if idx != len(errors) - 1:
+ # remove last 7 chars eg: ', 3095 '
+ errors[idx] = errors[idx][:-6]
+
+ return errors
+
+ def raise_error(self, raise_exception=False, errors=None):
+ if errors is None:
+ errors = []
+ title = _('E Invoice Request Failed')
+ if errors:
+ frappe.throw(errors, title=title, as_list=1)
+ else:
+ link_to_error_list = '
Error Log '
+ frappe.msgprint(
+ _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
+ title=title,
+ raise_exception=raise_exception,
+ indicator='red'
+ )
+
+ def set_einvoice_data(self, res):
+ enc_signed_invoice = res.get('SignedInvoice')
+ dec_signed_invoice = jwt.decode(enc_signed_invoice, options={"verify_signature": False})['data']
+
+ self.invoice.irn = res.get('Irn')
+ self.invoice.ewaybill = res.get('EwbNo')
+ self.invoice.eway_bill_validity = res.get('EwbValidTill')
+ self.invoice.ack_no = res.get('AckNo')
+ self.invoice.ack_date = res.get('AckDt')
+ self.invoice.signed_einvoice = dec_signed_invoice
+ self.invoice.ack_no = res.get('AckNo')
+ self.invoice.ack_date = res.get('AckDt')
+ self.invoice.signed_qr_code = res.get('SignedQRCode')
+ self.invoice.einvoice_status = 'Generated'
+
+ self.attach_qrcode_image()
+
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('IRN Generated')
+ }
+ self.update_invoice()
+
+ def attach_qrcode_image(self):
+ qrcode = self.invoice.signed_qr_code
+ doctype = self.invoice.doctype
+ docname = self.invoice.name
+ filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
+
+ qr_image = io.BytesIO()
+ url = qrcreate(qrcode, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": "qrcode_image",
+ "is_private": 0,
+ "content": qr_image.getvalue()})
+ _file.save()
+ frappe.db.commit()
+ self.invoice.qrcode_image = _file.file_url
+
+ def update_invoice(self):
+ self.invoice.flags.ignore_validate_update_after_submit = True
+ self.invoice.flags.ignore_validate = True
+ self.invoice.save()
+
+ def set_failed_status(self, errors=None):
+ frappe.db.rollback()
+ self.invoice.einvoice_status = 'Failed'
+ self.invoice.failure_description = self.get_failure_message(errors) if errors else ""
+ self.update_invoice()
+ frappe.db.commit()
+
+ def get_failure_message(self, errors):
+ if isinstance(errors, list):
+ errors = ', '.join(errors)
+ return errors
+
+def sanitize_for_json(string):
+ """Escape JSON specific characters from a string."""
+
+ # json.dumps adds double-quotes to the string. Indexing to remove them.
+ return json.dumps(string)[1:-1]
+
+@frappe.whitelist()
+def get_einvoice(doctype, docname):
+ invoice = frappe.get_doc(doctype, docname)
+ return make_einvoice(invoice)
+
+@frappe.whitelist()
+def generate_irn(doctype, docname):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.generate_irn()
+
+@frappe.whitelist()
+def cancel_irn(doctype, docname, irn, reason, remark):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.cancel_irn(irn, reason, remark)
+
+@frappe.whitelist()
+def generate_eway_bill(doctype, docname, **kwargs):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.generate_eway_bill(**kwargs)
+
+@frappe.whitelist()
+def cancel_eway_bill(doctype, docname):
+ # TODO: uncomment when eway_bill api from Adequare is enabled
+ # gsp_connector = GSPConnector(doctype, docname)
+ # gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+
+ frappe.db.set_value(doctype, docname, 'ewaybill', '')
+ frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
+
+@frappe.whitelist()
+def generate_einvoices(docnames):
+ docnames = json.loads(docnames) or []
+
+ if len(docnames) < 10:
+ failures = GSPConnector.bulk_generate_irn(docnames)
+ frappe.local.message_log = []
+
+ if failures:
+ show_bulk_action_failure_message(failures)
+
+ success = len(docnames) - len(failures)
+ frappe.msgprint(
+ _('{} e-invoices generated successfully').format(success),
+ title=_('Bulk E-Invoice Generation Complete')
+ )
+
+ else:
+ enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames)
+
+def schedule_bulk_generate_irn(docnames):
+ failures = GSPConnector.bulk_generate_irn(docnames)
+ frappe.local.message_log = []
+
+ frappe.publish_realtime("bulk_einvoice_generation_complete", {
+ "user": frappe.session.user,
+ "failures": failures,
+ "invoices": docnames
+ })
+
+def show_bulk_action_failure_message(failures):
+ for doc in failures:
+ docname = '
{0} '.format(doc.get('docname'))
+ message = doc.get('message').replace("'", '"')
+ if message[0] == '[':
+ errors = json.loads(message)
+ error_list = ''.join(['
{} '.format(err) for err in errors])
+ message = '''{} has following errors:
+
'''.format(docname, error_list)
+ else:
+ message = '{} - {}'.format(docname, message)
+
+ frappe.msgprint(
+ message,
+ title=_('Bulk E-Invoice Generation Complete'),
+ indicator='red'
+ )
+
+@frappe.whitelist()
+def cancel_irns(docnames, reason, remark):
+ docnames = json.loads(docnames) or []
+
+ if len(docnames) < 10:
+ failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
+ frappe.local.message_log = []
+
+ if failures:
+ show_bulk_action_failure_message(failures)
+
+ success = len(docnames) - len(failures)
+ frappe.msgprint(
+ _('{} e-invoices cancelled successfully').format(success),
+ title=_('Bulk E-Invoice Cancellation Complete')
+ )
+ else:
+ enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark)
+
+def schedule_bulk_cancel_irn(docnames, reason, remark):
+ failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
+ frappe.local.message_log = []
+
+ frappe.publish_realtime("bulk_einvoice_cancellation_complete", {
+ "user": frappe.session.user,
+ "failures": failures,
+ "invoices": docnames
+ })
+
+def enqueue_bulk_action(job, **kwargs):
+ check_scheduler_status()
+
+ enqueue(
+ job,
+ **kwargs,
+ queue="long",
+ timeout=10000,
+ event="processing_bulk_einvoice_action",
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
+ )
+
+ if job == schedule_bulk_generate_irn:
+ msg = _('E-Invoices will be generated in a background process.')
+ else:
+ msg = _('E-Invoices will be cancelled in a background process.')
+
+ frappe.msgprint(msg, alert=1)
+
+def check_scheduler_status():
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+
+def job_already_enqueued(job_name):
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+ if job_name in enqueued_jobs:
+ return True
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index c0dcb70b92..074bd527e2 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -60,7 +60,7 @@ def create_hsn_codes(data, code_field):
def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register',
- 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'):
+ 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'):
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
@@ -99,7 +99,7 @@ def add_custom_roles_for_reports():
)).insert()
def add_permissions():
- for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
+ for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'):
add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0)
@@ -115,9 +115,11 @@ def add_permissions():
def add_print_formats():
frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
+ frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters
@@ -453,7 +455,7 @@ def get_custom_fields():
'fieldname': 'ewaybill',
'label': 'E-Way Bill No.',
'fieldtype': 'Data',
- 'depends_on': 'eval:(doc.docstatus === 1)',
+ 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)',
'allow_on_submit': 1,
'insert_after': 'tax_id',
'translatable': 0,
@@ -481,6 +483,46 @@ def get_custom_fields():
fetch_from='customer_address.gstin', print_hide=1, read_only=1)
]
+ si_einvoice_fields = [
+ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
+ depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
+
+ dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1,
+ depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'),
+
+ dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
+ print_hide=1, hidden=1),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
+ options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
+ hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
+ ]
+
custom_fields = {
'Address': [
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
@@ -493,7 +535,7 @@ def get_custom_fields():
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields,
- 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields,
+ 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'POS Invoice': sales_invoice_gst_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Payment Entry': payment_entry_fields,
@@ -567,16 +609,16 @@ def get_custom_fields():
fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
dict(fieldname='arrear_component', label='Arrear Component',
- fieldtype='Link', options='Salary Component', insert_after='hra_component'),
+ fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
dict(fieldname='non_profit_section', label='Non Profit Settings',
- fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
dict(fieldname='company_80g_number', label='80G Number',
fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
dict(fieldname='pan_details', label='PAN Number',
- fieldtype='Data', insert_after='with_effect_from')
+ fieldtype='Data', insert_after='non_profit_column_break')
],
'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption',
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index d443f9c15c..8715ef57ba 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -219,6 +219,7 @@ def get_regional_address_details(party_details, doctype, company):
if not party_details.place_of_supply: return party_details
if not party_details.company_gstin: return party_details
+ if not party_details.supplier_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
index 14d5495eed..052fb2a724 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -1,9 +1,9 @@
import zipfile
+from io import BytesIO
from unittest import TestCase
import frappe
from frappe.utils import cstr, now_datetime, today
-from six import BytesIO
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.germany.utils.datev.datev_constants import (
diff --git a/erpnext/restaurant/doctype/restaurant_menu/__init__.py b/erpnext/regional/report/e_invoice_summary/__init__.py
similarity index 100%
rename from erpnext/restaurant/doctype/restaurant_menu/__init__.py
rename to erpnext/regional/report/e_invoice_summary/__init__.py
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js
new file mode 100644
index 0000000000..4713217d83
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js
@@ -0,0 +1,55 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["E-Invoice Summary"] = {
+ "filters": [
+ {
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "fieldname": "company",
+ "label": __("Company"),
+ "default": frappe.defaults.get_user_default("Company"),
+ },
+ {
+ "fieldtype": "Link",
+ "options": "Customer",
+ "fieldname": "customer",
+ "label": __("Customer")
+ },
+ {
+ "fieldtype": "Date",
+ "reqd": 1,
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ "fieldtype": "Date",
+ "reqd": 1,
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "default": frappe.datetime.get_today(),
+ },
+ {
+ "fieldtype": "Select",
+ "fieldname": "status",
+ "label": __("Status"),
+ "options": "\nPending\nGenerated\nCancelled\nFailed"
+ }
+ ],
+
+ "formatter": function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "einvoice_status" && value) {
+ if (value == 'Pending') value = `
${value} `;
+ else if (value == 'Generated') value = `
${value} `;
+ else if (value == 'Cancelled') value = `
${value} `;
+ else if (value == 'Failed') value = `
${value} `;
+ }
+
+ return value;
+ }
+};
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
new file mode 100644
index 0000000000..d0000ad50d
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
@@ -0,0 +1,28 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-03-12 11:23:37.312294",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letter_head": "Logo",
+ "modified": "2021-03-13 12:36:48.689413",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E-Invoice Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Invoice",
+ "report_name": "E-Invoice Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Administrator"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
new file mode 100644
index 0000000000..2110c44447
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+
+
+def execute(filters=None):
+ validate_filters(filters)
+
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def validate_filters(filters=None):
+ if filters is None:
+ filters = {}
+ filters = frappe._dict(filters)
+
+ if not filters.company:
+ frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter'))
+ if filters.company:
+ # validate if company has e-invoicing enabled
+ pass
+ if not filters.from_date or not filters.to_date:
+ frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter'))
+ if filters.from_date > filters.to_date:
+ frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter'))
+
+def get_data(filters=None):
+ if filters is None:
+ filters = {}
+ query_filters = {
+ 'posting_date': ['between', [filters.from_date, filters.to_date]],
+ 'einvoice_status': ['is', 'set'],
+ 'company': filters.company
+ }
+ if filters.customer:
+ query_filters['customer'] = filters.customer
+ if filters.status:
+ query_filters['einvoice_status'] = filters.status
+
+ data = frappe.get_all(
+ 'Sales Invoice',
+ filters=query_filters,
+ fields=[d.get('fieldname') for d in get_columns()]
+ )
+
+ return data
+
+def get_columns():
+ return [
+ {
+ "fieldtype": "Date",
+ "fieldname": "posting_date",
+ "label": _("Posting Date"),
+ "width": 0
+ },
+ {
+ "fieldtype": "Link",
+ "fieldname": "name",
+ "label": _("Sales Invoice"),
+ "options": "Sales Invoice",
+ "width": 140
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "einvoice_status",
+ "label": _("Status"),
+ "width": 100
+ },
+ {
+ "fieldtype": "Link",
+ "fieldname": "customer",
+ "options": "Customer",
+ "label": _("Customer")
+ },
+ {
+ "fieldtype": "Check",
+ "fieldname": "is_return",
+ "label": _("Is Return"),
+ "width": 85
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "ack_no",
+ "label": "Ack. No.",
+ "width": 145
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "ack_date",
+ "label": "Ack. Date",
+ "width": 165
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "irn",
+ "label": _("IRN No."),
+ "width": 250
+ },
+ {
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "fieldname": "base_grand_total",
+ "label": _("Grand Total"),
+ "width": 120
+ }
+ ]
diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js
index ef2bdb6798..4b98978f13 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.js
+++ b/erpnext/regional/report/gstr_1/gstr_1.js
@@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = {
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") },
- { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") }
+ { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") },
+ { "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") }
],
"default": "B2B"
}
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 11b684d3f6..e50ff18032 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -40,7 +40,8 @@ class Gstr1Report(object):
port_code,
shipping_bill_number,
shipping_bill_date,
- reason_for_issuing_document
+ reason_for_issuing_document,
+ company_gstin
"""
def run(self):
@@ -62,6 +63,8 @@ class Gstr1Report(object):
self.get_b2c_data()
elif self.filters.get("type_of_business") == "Advances":
self.get_advance_data()
+ elif self.filters.get("type_of_business") == "NIL Rated":
+ self.get_nil_rated_invoices()
elif self.invoices:
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
@@ -91,6 +94,57 @@ class Gstr1Report(object):
row= [key[0], key[1], value[0], value[1]]
self.data.append(row)
+ def get_nil_rated_invoices(self):
+ nil_exempt_output = [
+ {
+ "description": "Inter-State supplies to registered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ },
+ {
+ "description": "Intra-State supplies to registered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ },
+ {
+ "description": "Inter-State supplies to unregistered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ },
+ {
+ "description": "Intra-State supplies to unregistered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ }
+ ]
+
+ for invoice, details in self.nil_exempt_non_gst.items():
+ invoice_detail = self.invoices.get(invoice)
+ if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"):
+ if is_inter_state(invoice_detail):
+ nil_exempt_output[0]["nil_rated"] += details[0]
+ nil_exempt_output[0]["exempted"] += details[1]
+ nil_exempt_output[0]["non_gst"] += details[2]
+ else:
+ nil_exempt_output[1]["nil_rated"] += details[0]
+ nil_exempt_output[1]["exempted"] += details[1]
+ nil_exempt_output[1]["non_gst"] += details[2]
+ else:
+ if is_inter_state(invoice_detail):
+ nil_exempt_output[2]["nil_rated"] += details[0]
+ nil_exempt_output[2]["exempted"] += details[1]
+ nil_exempt_output[2]["non_gst"] += details[2]
+ else:
+ nil_exempt_output[3]["nil_rated"] += details[0]
+ nil_exempt_output[3]["exempted"] += details[1]
+ nil_exempt_output[3]["non_gst"] += details[2]
+
+ self.data = nil_exempt_output
+
def get_b2c_data(self):
b2cs_output = {}
@@ -240,10 +294,11 @@ class Gstr1Report(object):
def get_invoice_items(self):
self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict()
+ self.nil_exempt_non_gst = {}
items = frappe.db.sql("""
- select item_code, parent, taxable_value, base_net_amount, item_tax_rate
- from `tab%s Item`
+ select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
+ is_non_gst from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@@ -260,6 +315,16 @@ class Gstr1Report(object):
tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
tax_rate_dict.append(rate)
+ if d.is_nil_exempt:
+ self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
+ if item_tax_rate:
+ self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0)
+ else:
+ self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0)
+ elif d.is_non_gst:
+ self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
+ self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0)
+
def get_items_based_on_tax_rate(self):
self.tax_details = frappe.db.sql("""
select
@@ -322,21 +387,24 @@ class Gstr1Report(object):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self):
- self.tax_columns = [
- {
- "fieldname": "rate",
- "label": "Rate",
- "fieldtype": "Int",
- "width": 60
- },
- {
- "fieldname": "taxable_value",
- "label": "Taxable Value",
- "fieldtype": "Currency",
- "width": 100
- }
- ]
self.other_columns = []
+ self.tax_columns = []
+
+ if self.filters.get("type_of_business") != "NIL Rated":
+ self.tax_columns = [
+ {
+ "fieldname": "rate",
+ "label": "Rate",
+ "fieldtype": "Int",
+ "width": 60
+ },
+ {
+ "fieldname": "taxable_value",
+ "label": "Taxable Value",
+ "fieldtype": "Currency",
+ "width": 100
+ }
+ ]
if self.filters.get("type_of_business") == "B2B":
self.invoice_columns = [
@@ -705,6 +773,33 @@ class Gstr1Report(object):
"width": 100
}
]
+ elif self.filters.get("type_of_business") == "NIL Rated":
+ self.invoice_columns = [
+ {
+ "fieldname": "description",
+ "label": "Description",
+ "fieldtype": "Data",
+ "width": 420
+ },
+ {
+ "fieldname": "nil_rated",
+ "label": "Nil Rated",
+ "fieldtype": "Currency",
+ "width": 200
+ },
+ {
+ "fieldname": "exempted",
+ "label": "Exempted",
+ "fieldtype": "Currency",
+ "width": 200
+ },
+ {
+ "fieldname": "non_gst",
+ "label": "Non GST",
+ "fieldtype": "Currency",
+ "width": 200
+ }
+ ]
self.columns = self.invoice_columns + self.tax_columns + self.other_columns
@@ -768,6 +863,11 @@ def get_json(filters, report_name, data):
out = get_advances_json(res, gstin)
gst_json["at"] = out
+ elif filters["type_of_business"] == "NIL Rated":
+ res = report_data[:-1]
+ out = get_exempted_json(res)
+ gst_json["nil"] = out
+
return {
'report_name': report_name,
'report_type': filters['type_of_business'],
@@ -980,6 +1080,36 @@ def get_cdnr_unreg_json(res, gstin):
return out
+def get_exempted_json(data):
+ out = {
+ "inv": [
+ {
+ "sply_ty": "INTRB2B"
+ },
+ {
+ "sply_ty": "INTRAB2B"
+ },
+ {
+ "sply_ty": "INTRB2C"
+ },
+ {
+ "sply_ty": "INTRAB2C"
+ }
+ ]
+ }
+
+ for i, v in enumerate(data):
+ if data[i].get('nil_rated'):
+ out['inv'][i]['nil_amt'] = data[i]['nil_rated']
+
+ if data[i].get('exempted'):
+ out['inv'][i]['expt_amt'] = data[i]['exempted']
+
+ if data[i].get('non_gst'):
+ out['inv'][i]['ngsup_amt'] = data[i]['non_gst']
+
+ return out
+
def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY':
@@ -1064,3 +1194,9 @@ def download_json_file():
frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
+
+def is_inter_state(invoice_detail):
+ if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]:
+ return True
+ else:
+ return False
\ No newline at end of file
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 2e31c03d5c..15d524d5b8 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -3,12 +3,10 @@
import frappe
from frappe.permissions import add_permission, update_permission_property
-from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup(company=None, patch=True):
- uae_custom_fields()
add_print_formats()
add_permissions()
make_custom_fields()
@@ -40,38 +38,67 @@ def make_custom_fields():
- Company Name in Arabic
- Address in Arabic
"""
+ is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
+ fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description',
+ print_hide=1)
+
+ is_exempt = dict(fieldname='is_exempt', label='Is Exempt',
+ fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated',
+ print_hide=1)
+
+ purchase_invoice_fields = [
+ dict(fieldname='company_trn', label='Company TRN',
+ fieldtype='Read Only', insert_after='shipping_address',
+ fetch_from='company.tax_id', print_hide=1),
+ dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic',
+ fieldtype='Read Only', insert_after='supplier_name',
+ fetch_from='supplier.supplier_name_in_arabic', print_hide=1)
+ ]
+
+ sales_invoice_fields = [
+ dict(fieldname='company_trn', label='Company TRN',
+ fieldtype='Read Only', insert_after='company_address',
+ fetch_from='company.tax_id', print_hide=1),
+ dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic',
+ fieldtype='Read Only', insert_after='customer_name',
+ fetch_from='customer.customer_name_in_arabic', print_hide=1),
+ dict(fieldname='ksa_einv_qr', label='KSA E-Invoicing QR',
+ fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1)
+ ]
+
custom_fields = {
- 'Sales Invoice': [
- dict(
- fieldname='ksa_einv_qr',
- label='KSA E-Invoicing QR',
- fieldtype='Attach Image',
- read_only=1, no_copy=1, hidden=1
- )
+ 'Item': [is_zero_rated, is_exempt],
+ 'Customer': [
+ dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic',
+ fieldtype='Data', insert_after='customer_name'),
],
- 'POS Invoice': [
- dict(
- fieldname='ksa_einv_qr',
- label='KSA E-Invoicing QR',
- fieldtype='Attach Image',
- read_only=1, no_copy=1, hidden=1
- )
+ 'Supplier': [
+ dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic',
+ fieldtype='Data', insert_after='supplier_name'),
],
+ 'Purchase Invoice': purchase_invoice_fields,
+ 'Purchase Order': purchase_invoice_fields,
+ 'Purchase Receipt': purchase_invoice_fields,
+ 'Sales Invoice': sales_invoice_fields,
+ 'POS Invoice': sales_invoice_fields,
+ 'Sales Order': sales_invoice_fields,
+ 'Delivery Note': sales_invoice_fields,
+ 'Sales Invoice Item': [is_zero_rated, is_exempt],
+ 'POS Invoice Item': [is_zero_rated, is_exempt],
+ 'Purchase Invoice Item': [is_zero_rated, is_exempt],
+ 'Sales Order Item': [is_zero_rated, is_exempt],
+ 'Delivery Note Item': [is_zero_rated, is_exempt],
+ 'Quotation Item': [is_zero_rated, is_exempt],
+ 'Purchase Order Item': [is_zero_rated, is_exempt],
+ 'Purchase Receipt Item': [is_zero_rated, is_exempt],
+ 'Supplier Quotation Item': [is_zero_rated, is_exempt],
'Address': [
- dict(
- fieldname='address_in_arabic',
- label='Address in Arabic',
- fieldtype='Data',
- insert_after='address_line2'
- )
+ dict(fieldname='address_in_arabic', label='Address in Arabic',
+ fieldtype='Data',insert_after='address_line2')
],
'Company': [
- dict(
- fieldname='company_name_in_arabic',
- label='Company Name In Arabic',
- fieldtype='Data',
- insert_after='company_name'
- )
+ dict(fieldname='company_name_in_arabic', label='Company Name In Arabic',
+ fieldtype='Data', insert_after='company_name')
]
}
diff --git a/erpnext/restaurant/doctype/restaurant/restaurant.js b/erpnext/restaurant/doctype/restaurant/restaurant.js
deleted file mode 100644
index 13fda73922..0000000000
--- a/erpnext/restaurant/doctype/restaurant/restaurant.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Restaurant', {
- refresh: function(frm) {
- frm.add_custom_button(__('Order Entry'), () => {
- frappe.set_route('Form', 'Restaurant Order Entry');
- });
- }
-});
diff --git a/erpnext/restaurant/doctype/restaurant/restaurant.json b/erpnext/restaurant/doctype/restaurant/restaurant.json
deleted file mode 100644
index 8572687411..0000000000
--- a/erpnext/restaurant/doctype/restaurant/restaurant.json
+++ /dev/null
@@ -1,309 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "prompt",
- "beta": 1,
- "creation": "2017-09-15 12:40:41.546933",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "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": "Image",
- "length": 0,
- "no_copy": 0,
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "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": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "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": "default_customer",
- "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": "Default 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": "invoice_series_prefix",
- "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": "Invoice Series Prefix",
- "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": "column_break_4",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "active_menu",
- "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": "Active Menu",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant Menu",
- "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": "default_tax_template",
- "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": "Default Tax Template",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Taxes and Charges Template",
- "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": "address",
- "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": "Address",
- "length": 0,
- "no_copy": 0,
- "options": "Address",
- "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_field": "image",
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-12-09 12:13:10.185496",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant",
- "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": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant/restaurant.py b/erpnext/restaurant/doctype/restaurant/restaurant.py
deleted file mode 100644
index 67838d29a3..0000000000
--- a/erpnext/restaurant/doctype/restaurant/restaurant.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class Restaurant(Document):
- pass
diff --git a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py
deleted file mode 100644
index bfdd052753..0000000000
--- a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- 'fieldname': 'restaurant',
- 'transactions': [
- {
- 'label': _('Setup'),
- 'items': ['Restaurant Menu', 'Restaurant Table']
- },
- {
- 'label': _('Operations'),
- 'items': ['Restaurant Reservation', 'Sales Invoice']
- }
- ]
- }
diff --git a/erpnext/restaurant/doctype/restaurant/test_restaurant.py b/erpnext/restaurant/doctype/restaurant/test_restaurant.py
deleted file mode 100644
index f88f980129..0000000000
--- a/erpnext/restaurant/doctype/restaurant/test_restaurant.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-test_records = [
- dict(doctype='Restaurant', name='Test Restaurant 1', company='_Test Company 1',
- invoice_series_prefix='Test-Rest-1-Inv-', default_customer='_Test Customer 1'),
- dict(doctype='Restaurant', name='Test Restaurant 2', company='_Test Company 1',
- invoice_series_prefix='Test-Rest-2-Inv-', default_customer='_Test Customer 1'),
-]
-
-class TestRestaurant(unittest.TestCase):
- pass
diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.js b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.js
deleted file mode 100644
index da7d43f8a3..0000000000
--- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Restaurant Menu', {
- setup: function(frm) {
- frm.add_fetch('item', 'standard_rate', 'rate');
- },
-});
diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.json b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.json
deleted file mode 100644
index 1b1610dbac..0000000000
--- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.json
+++ /dev/null
@@ -1,247 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "prompt",
- "beta": 1,
- "creation": "2017-09-15 12:48:29.818715",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "restaurant",
- "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": "Restaurant",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant",
- "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,
- "default": "1",
- "fieldname": "enabled",
- "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": "Enabled",
- "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": "column_break_3",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "price_list",
- "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": "Price List (Auto created)",
- "length": 0,
- "no_copy": 0,
- "options": "Price List",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "items_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": "Items",
- "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": "items",
- "fieldtype": "Table",
- "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": "Items",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant Menu Item",
- "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
- }
- ],
- "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": "2017-12-09 12:13:13.684500",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant Menu",
- "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": "Restaurant Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py
deleted file mode 100644
index 64eb40f364..0000000000
--- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-from frappe.model.document import Document
-
-
-class RestaurantMenu(Document):
- def validate(self):
- for d in self.items:
- if not d.rate:
- d.rate = frappe.db.get_value('Item', d.item, 'standard_rate')
-
- def on_update(self):
- '''Sync Price List'''
- self.make_price_list()
-
- def on_trash(self):
- '''clear prices'''
- self.clear_item_price()
-
- def clear_item_price(self, price_list=None):
- '''clear all item prices for this menu'''
- if not price_list:
- price_list = self.get_price_list().name
- frappe.db.sql('delete from `tabItem Price` where price_list = %s', price_list)
-
- def make_price_list(self):
- # create price list for menu
- price_list = self.get_price_list()
- self.db_set('price_list', price_list.name)
-
- # delete old items
- self.clear_item_price(price_list.name)
-
- for d in self.items:
- frappe.get_doc(dict(
- doctype = 'Item Price',
- price_list = price_list.name,
- item_code = d.item,
- price_list_rate = d.rate
- )).insert()
-
- def get_price_list(self):
- '''Create price list for menu if missing'''
- price_list_name = frappe.db.get_value('Price List', dict(restaurant_menu=self.name))
- if price_list_name:
- price_list = frappe.get_doc('Price List', price_list_name)
- else:
- price_list = frappe.new_doc('Price List')
- price_list.restaurant_menu = self.name
- price_list.price_list_name = self.name
-
- price_list.enabled = 1
- price_list.selling = 1
- price_list.save()
-
- return price_list
diff --git a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py
deleted file mode 100644
index 27020eb869..0000000000
--- a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-test_records = [
- dict(doctype='Item', item_code='Food Item 1',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Food Item 2',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Food Item 3',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Food Item 4',
- item_group='Products', is_stock_item=0),
- dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 1',
- items = [
- dict(item='Food Item 1', rate=400),
- dict(item='Food Item 2', rate=300),
- dict(item='Food Item 3', rate=200),
- dict(item='Food Item 4', rate=100),
- ]),
- dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 2',
- items = [
- dict(item='Food Item 1', rate=450),
- dict(item='Food Item 2', rate=350),
- ])
-]
-
-class TestRestaurantMenu(unittest.TestCase):
- def test_price_list_creation_and_editing(self):
- menu1 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 1')
- menu1.save()
-
- menu2 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 2')
- menu2.save()
-
- self.assertTrue(frappe.db.get_value('Price List', 'Test Restaurant 1 Menu 1'))
- self.assertEqual(frappe.db.get_value('Item Price',
- dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 400)
- self.assertEqual(frappe.db.get_value('Item Price',
- dict(price_list = 'Test Restaurant 1 Menu 2', item_code='Food Item 1'), 'price_list_rate'), 450)
-
- menu1.items[0].rate = 401
- menu1.save()
-
- self.assertEqual(frappe.db.get_value('Item Price',
- dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 401)
-
- menu1.items[0].rate = 400
- menu1.save()
diff --git a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.json b/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.json
deleted file mode 100644
index 87568bf981..0000000000
--- a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-09-15 12:49:36.072636",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item",
- "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": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "rate",
- "fieldtype": "Currency",
- "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": "Rate",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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": 1,
- "max_attachments": 0,
- "modified": "2017-09-15 14:18:55.145088",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant Menu Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.py b/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.py
deleted file mode 100644
index 98b245edec..0000000000
--- a/erpnext/restaurant/doctype/restaurant_menu_item/restaurant_menu_item.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class RestaurantMenuItem(Document):
- pass
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/__init__.py b/erpnext/restaurant/doctype/restaurant_order_entry/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js
deleted file mode 100644
index 8df851c62b..0000000000
--- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Restaurant Order Entry', {
- setup: function(frm) {
- let get_item_query = () => {
- return {
- query: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.item_query_restaurant',
- filters: {
- 'table': frm.doc.restaurant_table
- }
- };
- };
- frm.set_query('item', 'items', get_item_query);
- frm.set_query('add_item', get_item_query);
- },
- onload_post_render: function(frm) {
- if(!frm.item_selector) {
- frm.item_selector = new erpnext.ItemSelector({
- frm: frm,
- item_field: 'item',
- item_query: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.item_query_restaurant',
- get_filters: () => {
- return {table: frm.doc.restaurant_table};
- }
- });
- }
-
- let $input = frm.get_field('add_item').$input;
-
- $input.on('keyup', function(e) {
- if (e.which===13) {
- if (frm.clear_item_timeout) {
- clearTimeout (frm.clear_item_timeout);
- }
-
- // clear the item input so user can enter a new item
- frm.clear_item_timeout = setTimeout (() => {
- frm.set_value('add_item', '');
- }, 1000);
-
- let item = $input.val();
-
- if (!item) return;
-
- var added = false;
- (frm.doc.items || []).forEach((d) => {
- if (d.item===item) {
- d.qty += 1;
- added = true;
- }
- });
-
- return frappe.run_serially([
- () => {
- if (!added) {
- return frm.add_child('items', {item: item, qty: 1});
- }
- },
- () => frm.get_field("items").refresh()
- ]);
- }
- });
- },
- refresh: function(frm) {
- frm.disable_save();
- frm.add_custom_button(__('Update'), () => {
- return frm.trigger('sync');
- });
- frm.add_custom_button(__('Clear'), () => {
- return frm.trigger('clear');
- });
- frm.add_custom_button(__('Bill'), () => {
- return frm.trigger('make_invoice');
- });
- },
- clear: function(frm) {
- frm.doc.add_item = '';
- frm.doc.grand_total = 0;
- frm.doc.items = [];
- frm.refresh();
- frm.get_field('add_item').$input.focus();
- },
- restaurant_table: function(frm) {
- // select the open sales order items for this table
- if (!frm.doc.restaurant_table) {
- return;
- }
- return frappe.call({
- method: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.get_invoice',
- args: {
- table: frm.doc.restaurant_table
- },
- callback: (r) => {
- frm.events.set_invoice_items(frm, r);
- }
- });
- },
- sync: function(frm) {
- return frappe.call({
- method: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.sync',
- args: {
- table: frm.doc.restaurant_table,
- items: frm.doc.items
- },
- callback: (r) => {
- frm.events.set_invoice_items(frm, r);
- frappe.show_alert({message: __('Saved'), indicator: 'green'});
- }
- });
-
- },
- make_invoice: function(frm) {
- frm.trigger('sync').then(() => {
- frappe.prompt([
- {
- fieldname: 'customer',
- label: __('Customer'),
- fieldtype: 'Link',
- reqd: 1,
- options: 'Customer',
- 'default': frm.invoice.customer
- },
- {
- fieldname: 'mode_of_payment',
- label: __('Mode of Payment'),
- fieldtype: 'Link',
- reqd: 1,
- options: 'Mode of Payment',
- 'default': frm.mode_of_payment || ''
- }
- ], (data) => {
- // cache this for next entry
- frm.mode_of_payment = data.mode_of_payment;
- return frappe.call({
- method: 'erpnext.restaurant.doctype.restaurant_order_entry.restaurant_order_entry.make_invoice',
- args: {
- table: frm.doc.restaurant_table,
- customer: data.customer,
- mode_of_payment: data.mode_of_payment
- },
- callback: (r) => {
- frm.set_value('last_sales_invoice', r.message);
- frm.trigger('clear');
- }
- });
- },
- __("Select Customer"));
- });
- },
- set_invoice_items: function(frm, r) {
- let invoice = r.message;
- frm.doc.items = [];
- (invoice.items || []).forEach((d) => {
- frm.add_child('items', {item: d.item_code, qty: d.qty, rate: d.rate});
- });
- frm.set_value('grand_total', invoice.grand_total);
- frm.set_value('last_sales_invoice', invoice.name);
- frm.invoice = invoice;
- frm.refresh();
- }
-});
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.json b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.json
deleted file mode 100644
index 3e4d593d5b..0000000000
--- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.json
+++ /dev/null
@@ -1,280 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 1,
- "creation": "2017-09-15 15:10:24.530365",
- "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": "restaurant_table",
- "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": "Restaurant Table",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant Table",
- "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,
- "depends_on": "restaurant_table",
- "description": "Click Enter To Add",
- "fieldname": "add_item",
- "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": "Add Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "column_break_3",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "last_sales_invoice",
- "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": "Last Sales Invoice",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Invoice",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "restaurant_table",
- "fieldname": "current_order",
- "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": "Current Order",
- "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,
- "depends_on": "restaurant_table",
- "fieldname": "items",
- "fieldtype": "Table",
- "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": "Items",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant Order Entry Item",
- "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": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-10-04 17:06:20.926999",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant Order Entry",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "Restaurant Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py
deleted file mode 100644
index f9e75b47a0..0000000000
--- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import json
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-
-from erpnext.controllers.queries import item_query
-
-
-class RestaurantOrderEntry(Document):
- pass
-
-@frappe.whitelist()
-def get_invoice(table):
- '''returns the active invoice linked to the given table'''
- invoice_name = frappe.get_value('Sales Invoice', dict(restaurant_table = table, docstatus=0))
- restaurant, menu_name = get_restaurant_and_menu_name(table)
- if invoice_name:
- invoice = frappe.get_doc('Sales Invoice', invoice_name)
- else:
- invoice = frappe.new_doc('Sales Invoice')
- invoice.naming_series = frappe.db.get_value('Restaurant', restaurant, 'invoice_series_prefix')
- invoice.is_pos = 1
- default_customer = frappe.db.get_value('Restaurant', restaurant, 'default_customer')
- if not default_customer:
- frappe.throw(_('Please set default customer in Restaurant Settings'))
- invoice.customer = default_customer
-
- invoice.taxes_and_charges = frappe.db.get_value('Restaurant', restaurant, 'default_tax_template')
- invoice.selling_price_list = frappe.db.get_value('Price List', dict(restaurant_menu=menu_name, enabled=1))
-
- return invoice
-
-@frappe.whitelist()
-def sync(table, items):
- '''Sync the sales order related to the table'''
- invoice = get_invoice(table)
- items = json.loads(items)
-
- invoice.items = []
- invoice.restaurant_table = table
- for d in items:
- invoice.append('items', dict(
- item_code = d.get('item'),
- qty = d.get('qty')
- ))
-
- invoice.save()
- return invoice.as_dict()
-
-@frappe.whitelist()
-def make_invoice(table, customer, mode_of_payment):
- '''Make table based on Sales Order'''
- restaurant, menu = get_restaurant_and_menu_name(table)
- invoice = get_invoice(table)
- invoice.customer = customer
- invoice.restaurant = restaurant
- invoice.calculate_taxes_and_totals()
- invoice.append('payments', dict(mode_of_payment=mode_of_payment, amount=invoice.grand_total))
- invoice.save()
- invoice.submit()
-
- frappe.msgprint(_('Invoice Created'), indicator='green', alert=True)
-
- return invoice.name
-
-@frappe.whitelist()
-def item_query_restaurant(doctype='Item', txt='', searchfield='name', start=0, page_len=20, filters=None, as_dict=False):
- '''Return items that are selected in active menu of the restaurant'''
- restaurant, menu = get_restaurant_and_menu_name(filters['table'])
- items = frappe.db.get_all('Restaurant Menu Item', ['item'], dict(parent = menu))
- del filters['table']
- filters['name'] = ('in', [d.item for d in items])
-
- return item_query('Item', txt, searchfield, start, page_len, filters, as_dict)
-
-def get_restaurant_and_menu_name(table):
- if not table:
- frappe.throw(_('Please select a table'))
-
- restaurant = frappe.db.get_value('Restaurant Table', table, 'restaurant')
- menu = frappe.db.get_value('Restaurant', restaurant, 'active_menu')
-
- if not menu:
- frappe.throw(_('Please set an active menu for Restaurant {0}').format(restaurant))
-
- return restaurant, menu
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry_item/__init__.py b/erpnext/restaurant/doctype/restaurant_order_entry_item/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.json b/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.json
deleted file mode 100644
index 0240013c78..0000000000
--- a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.json
+++ /dev/null
@@ -1,163 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-09-15 15:11:50.313241",
- "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": "item",
- "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": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "qty",
- "fieldtype": "Int",
- "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": "Qty",
- "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": "served",
- "fieldtype": "Int",
- "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": "Served",
- "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": "rate",
- "fieldtype": "Currency",
- "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": "Rate",
- "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": 1,
- "max_attachments": 0,
- "modified": "2017-09-21 08:39:27.232175",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant Order Entry Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.py b/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.py
deleted file mode 100644
index 0d9c236c0e..0000000000
--- a/erpnext/restaurant/doctype/restaurant_order_entry_item/restaurant_order_entry_item.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from frappe.model.document import Document
-
-
-class RestaurantOrderEntryItem(Document):
- pass
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/__init__.py b/erpnext/restaurant/doctype/restaurant_reservation/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.js b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.js
deleted file mode 100644
index cebd1052a8..0000000000
--- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Restaurant Reservation', {
- setup: function(frm) {
- frm.add_fetch('customer', 'customer_name', 'customer_name');
- },
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.json b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.json
deleted file mode 100644
index 17df2b931d..0000000000
--- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.json
+++ /dev/null
@@ -1,355 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "RES-RES-.YYYY.-.#####",
- "beta": 1,
- "creation": "2017-09-15 13:05:51.063661",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "status",
- "fieldtype": "Select",
- "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": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "Open\nWaitlisted\nCancelled\nNo Show\nSuccess",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "restaurant",
- "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": "Restaurant",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "no_of_people",
- "fieldtype": "Int",
- "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": "No of People",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reservation_time",
- "fieldtype": "Datetime",
- "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": "Reservation Time",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reservation_end_time",
- "fieldtype": "Datetime",
- "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": "Reservation End Time",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 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": 0,
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Customer 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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact_number",
- "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": "Contact Number",
- "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
- }
- ],
- "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-08-21 16:15:38.435656",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant Reservation",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 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": "Restaurant Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.py b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.py
deleted file mode 100644
index 02ffaf6c20..0000000000
--- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from datetime import timedelta
-
-from frappe.model.document import Document
-from frappe.utils import get_datetime
-
-
-class RestaurantReservation(Document):
- def validate(self):
- if not self.reservation_end_time:
- self.reservation_end_time = get_datetime(self.reservation_time) + timedelta(hours=1)
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation_calendar.js b/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation_calendar.js
deleted file mode 100644
index fe3dc57a72..0000000000
--- a/erpnext/restaurant/doctype/restaurant_reservation/restaurant_reservation_calendar.js
+++ /dev/null
@@ -1,18 +0,0 @@
-frappe.views.calendar["Restaurant Reservation"] = {
- field_map: {
- "start": "reservation_time",
- "end": "reservation_end_time",
- "id": "name",
- "title": "customer_name",
- "allDay": "allDay",
- },
- gantt: true,
- filters: [
- {
- "fieldtype": "Data",
- "fieldname": "customer_name",
- "label": __("Customer Name")
- }
- ],
- get_events_method: "frappe.desk.calendar.get_events"
-};
diff --git a/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.py b/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.py
deleted file mode 100644
index 11a3541bd5..0000000000
--- a/erpnext/restaurant/doctype/restaurant_reservation/test_restaurant_reservation.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestRestaurantReservation(unittest.TestCase):
- pass
diff --git a/erpnext/restaurant/doctype/restaurant_table/__init__.py b/erpnext/restaurant/doctype/restaurant_table/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.js b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.js
deleted file mode 100644
index a55605c90b..0000000000
--- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Restaurant Table', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.json b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.json
deleted file mode 100644
index 5fc6e62ecb..0000000000
--- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.json
+++ /dev/null
@@ -1,156 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 1,
- "creation": "2017-09-15 12:45:24.717355",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "restaurant",
- "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": "Restaurant",
- "length": 0,
- "no_copy": 0,
- "options": "Restaurant",
- "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": "no_of_seats",
- "fieldtype": "Int",
- "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": "No of Seats",
- "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,
- "default": "1",
- "fieldname": "minimum_seating",
- "fieldtype": "Int",
- "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": "Minimum Seating",
- "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
- }
- ],
- "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": "2017-12-09 12:13:24.382345",
- "modified_by": "Administrator",
- "module": "Restaurant",
- "name": "Restaurant Table",
- "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": "Restaurant Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Hospitality",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py
deleted file mode 100644
index 29f8a1a12b..0000000000
--- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import re
-
-from frappe.model.document import Document
-from frappe.model.naming import make_autoname
-
-
-class RestaurantTable(Document):
- def autoname(self):
- prefix = re.sub('-+', '-', self.restaurant.replace(' ', '-'))
- self.name = make_autoname(prefix + '-.##')
diff --git a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py
deleted file mode 100644
index 00d14d2bb2..0000000000
--- a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-test_records = [
- dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1),
- dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1),
- dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1),
- dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1),
-]
-
-class TestRestaurantTable(unittest.TestCase):
- pass
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index b7f74df105..7742f26ad1 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -142,7 +142,7 @@ class Customer(TransactionBase):
self.update_lead_status()
if self.flags.is_new_doc:
- self.create_lead_address_contact()
+ self.link_lead_address_and_contact()
self.update_customer_groups()
@@ -176,62 +176,24 @@ class Customer(TransactionBase):
if self.lead_name:
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
- def create_lead_address_contact(self):
+ def link_lead_address_and_contact(self):
if self.lead_name:
- # assign lead address to customer (if already not set)
- address_names = frappe.get_all('Dynamic Link', filters={
- "parenttype":"Address",
- "link_doctype":"Lead",
- "link_name":self.lead_name
- }, fields=["parent as name"])
+ # assign lead address and contact to customer (if already not set)
+ linked_contacts_and_addresses = frappe.get_all(
+ "Dynamic Link",
+ filters=[
+ ["parenttype", "in", ["Contact", "Address"]],
+ ["link_doctype", "=", "Lead"],
+ ["link_name", "=", self.lead_name],
+ ],
+ fields=["parent as name", "parenttype as doctype"],
+ )
- for address_name in address_names:
- address = frappe.get_doc('Address', address_name.get('name'))
- if not address.has_link('Customer', self.name):
- address.append('links', dict(link_doctype='Customer', link_name=self.name))
- address.save(ignore_permissions=self.flags.ignore_permissions)
-
- lead = frappe.db.get_value("Lead", self.lead_name, ["company_name", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
-
- if not lead.lead_name:
- frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name))
-
- contact_names = frappe.get_all('Dynamic Link', filters={
- "parenttype":"Contact",
- "link_doctype":"Lead",
- "link_name":self.lead_name
- }, fields=["parent as name"])
-
- for contact_name in contact_names:
- contact = frappe.get_doc('Contact', contact_name.get('name'))
- if not contact.has_link('Customer', self.name):
- contact.append('links', dict(link_doctype='Customer', link_name=self.name))
- contact.save(ignore_permissions=self.flags.ignore_permissions)
-
- if not contact_names:
- lead.lead_name = lead.lead_name.lstrip().split(" ")
- lead.first_name = lead.lead_name[0]
- lead.last_name = " ".join(lead.lead_name[1:])
-
- # create contact from lead
- contact = frappe.new_doc('Contact')
- contact.first_name = lead.first_name
- contact.last_name = lead.last_name
- contact.gender = lead.gender
- contact.salutation = lead.salutation
- contact.email_id = lead.email_id
- contact.phone = lead.phone
- contact.mobile_no = lead.mobile_no
- contact.is_primary_contact = 1
- contact.append('links', dict(link_doctype='Customer', link_name=self.name))
- if lead.email_id:
- contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1))
- if lead.mobile_no:
- contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1))
- contact.flags.ignore_permissions = self.flags.ignore_permissions
- contact.autoname()
- if not frappe.db.exists("Contact", contact.name):
- contact.insert()
+ for row in linked_contacts_and_addresses:
+ linked_doc = frappe.get_doc(row.doctype, row.name)
+ if not linked_doc.has_link('Customer', self.name):
+ linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name))
+ linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
def validate_name_with_customer_group(self):
if frappe.db.exists("Customer Group", self.name):
@@ -296,30 +258,6 @@ class Customer(TransactionBase):
.format(frappe.bold(self.customer_name))
)
- def create_onboarding_docs(self, args):
- defaults = frappe.defaults.get_defaults()
- company = defaults.get('company') or \
- frappe.db.get_single_value('Global Defaults', 'default_company')
-
- for i in range(1, args.get('max_count')):
- customer = args.get('customer_name_' + str(i))
- if customer:
- try:
- doc = frappe.get_doc({
- 'doctype': self.doctype,
- 'customer_name': customer,
- 'customer_type': 'Company',
- 'customer_group': _('Commercial'),
- 'territory': defaults.get('country'),
- 'company': company
- }).insert()
-
- if args.get('customer_email_' + str(i)):
- create_contact(customer, self.doctype,
- doc.name, args.get("customer_email_" + str(i)))
- except frappe.NameError:
- pass
-
def create_contact(contact, party_type, party, email):
"""Create contact based on given contact name"""
contact = contact.split(' ')
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index daab6fbb8f..eebde766d3 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -287,7 +287,7 @@ def _make_customer(source_name, ignore_permissions=False):
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("Shopping Cart Settings", None,
+ customer.customer_group = frappe.db.get_value("E Commerce Settings", None,
"default_customer_group")
try:
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index 8b53902d32..31a95896bc 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -649,7 +649,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:13:54.670763",
+ "modified": "2021-07-15 12:40:51.074820",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 79e9e17e41..eb98e6c0bf 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -457,12 +457,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_delivery_note_based_on_delivery_date() {
var me = this;
- var delivery_dates = [];
- $.each(this.frm.doc.items || [], function(i, d) {
- if(!delivery_dates.includes(d.delivery_date)) {
- delivery_dates.push(d.delivery_date);
- }
- });
+ var delivery_dates = this.frm.doc.items.map(i => i.delivery_date);
+ delivery_dates = [ ...new Set(delivery_dates) ];
var item_grid = this.frm.fields_dict["items"].grid;
if(!item_grid.get_selected().length && delivery_dates.length > 1) {
@@ -500,14 +496,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if(!dates) return;
- $.each(dates, function(i, d) {
- $.each(item_grid.grid_rows || [], function(j, row) {
- if(row.doc.delivery_date == d) {
- row.doc.__checked = 1;
- }
- });
- })
- me.make_delivery_note();
+ me.make_delivery_note(dates);
dialog.hide();
});
dialog.show();
@@ -516,10 +505,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
- make_delivery_note() {
+ make_delivery_note(delivery_dates) {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
- frm: this.frm
+ frm: this.frm,
+ args: {
+ delivery_dates
+ }
})
}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index cc951850a4..0f5b1e3b89 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -565,6 +565,13 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
}
if not skip_item_mapping:
+ def condition(doc):
+ # make_mapped_doc sets js `args` into `frappe.flags.args`
+ if frappe.flags.args and frappe.flags.args.delivery_dates:
+ if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates:
+ return False
+ return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+
mapper["Sales Order Item"] = {
"doctype": "Delivery Note Item",
"field_map": {
@@ -573,7 +580,7 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
"parent": "against_sales_order",
},
"postprocess": update_item,
- "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+ "condition": condition
}
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 42bc0b70f8..acf048e116 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1375,6 +1375,30 @@ class TestSalesOrder(ERPNextTestCase):
automatically_fetch_payment_terms(enable=0)
+ def test_zero_amount_sales_order_billing_status(self):
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ so = make_sales_order(uom="Nos", do_not_save=1)
+ so.items[0].rate = 0
+ so.save()
+ so.submit()
+
+ self.assertEqual(so.net_total, 0)
+ self.assertEqual(so.billing_status, 'Not Billed')
+
+ si = create_sales_invoice(qty=10, do_not_save=1)
+ si.price_list = '_Test Price List'
+ si.items[0].rate = 0
+ si.items[0].price_list_rate = 0
+ si.items[0].sales_order = so.name
+ si.items[0].so_detail = so.items[0].name
+ si.save()
+ si.submit()
+
+ self.assertEqual(si.net_total, 0)
+ so.load_from_db()
+ self.assertEqual(so.billing_status, 'Fully Billed')
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 27bc541d62..7c4a3f63dc 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -80,7 +80,7 @@
"description": "How often should Project and Company be updated based on Sales Transactions?",
"fieldname": "sales_update_frequency",
"fieldtype": "Select",
- "label": "Sales Update Frequency",
+ "label": "Sales Update Frequency in Company and Project",
"options": "Each Transaction\nDaily\nMonthly",
"reqd": 1
},
@@ -171,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-13 12:32:17.004404",
+ "modified": "2022-02-04 15:41:59.939261",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@@ -189,5 +189,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index fb86e614b6..e1ef63578e 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -16,7 +16,7 @@ class SellingSettings(Document):
self.toggle_editable_rate_for_bundle_items()
def validate(self):
- for key in ["cust_master_name", "campaign_naming_by", "customer_group", "territory",
+ for key in ["cust_master_name", "customer_group", "territory",
"maintain_same_sales_rate", "editable_price_list_rate", "selling_price_list"]:
frappe.db.set_default(key, self.get(key, ""))
diff --git a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json
deleted file mode 100644
index 92d00bcb38..0000000000
--- a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "add_more_button": 1,
- "app": "ERPNext",
- "creation": "2019-11-15 14:44:10.065014",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [
- {
- "label": "Learn More",
- "video_id": "zsrrVDk6VBs"
- }
- ],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:54:01.686006",
- "modified_by": "Administrator",
- "name": "Add A Few Customers",
- "owner": "Administrator",
- "ref_doctype": "Customer",
- "slide_desc": "",
- "slide_fields": [
- {
- "align": "",
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "label": "Customer Name",
- "placeholder": "",
- "reqd": 1
- },
- {
- "align": "",
- "fieldtype": "Column Break",
- "reqd": 0
- },
- {
- "align": "",
- "fieldname": "customer_email",
- "fieldtype": "Data",
- "label": "Email ID",
- "reqd": 1
- }
- ],
- "slide_order": 40,
- "slide_title": "Add A Few Customers",
- "slide_type": "Create"
-}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index db5b20e3e1..993c61d563 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -24,7 +24,7 @@ def search_by_term(search_term, warehouse, price_list):
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
as_dict=1)
- item_stock_qty = get_stock_availability(item_code, warehouse)
+ item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
price_list_rate, currency = frappe.db.get_value('Item Price', {
'price_list': price_list,
'item_code': item_code
@@ -99,7 +99,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
), {'warehouse': warehouse}, as_dict=1)
if items_data:
- items_data = filter_service_items(items_data)
items = [d.item_code for d in items_data]
item_prices_data = frappe.get_all("Item Price",
fields = ["item_code", "price_list_rate", "currency"],
@@ -112,7 +111,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
for item in items_data:
item_code = item.item_code
item_price = item_prices.get(item_code) or {}
- item_stock_qty = get_stock_availability(item_code, warehouse)
+ item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
row = {}
row.update(item)
@@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value):
return {}
-def filter_service_items(items):
- for item in items:
- if not item['is_stock_item']:
- if not frappe.db.exists('Product Bundle', item['item_code']):
- items.remove(item)
-
- return items
-
def get_conditions(search_term):
condition = "("
condition += """item.name like {search_term}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ce74f6d0a5..ea8459f970 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -248,7 +248,7 @@ erpnext.PointOfSale.Controller = class {
numpad_event: (value, action) => this.update_item_field(value, action),
- checkout: () => this.payment.checkout(),
+ checkout: () => this.save_and_checkout(),
edit_cart: () => this.payment.edit_cart(),
@@ -630,18 +630,24 @@ erpnext.PointOfSale.Controller = class {
}
async check_stock_availability(item_row, qty_needed, warehouse) {
- const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+ const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+ const available_qty = resp[0];
+ const is_stock_item = resp[1];
frappe.dom.unfreeze();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold()
if (!(available_qty > 0)) {
- frappe.model.clear_doc(item_row.doctype, item_row.name);
- frappe.throw({
- title: __("Not Available"),
- message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
- })
+ if (is_stock_item) {
+ frappe.model.clear_doc(item_row.doctype, item_row.name);
+ frappe.throw({
+ title: __("Not Available"),
+ message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
+ });
+ } else {
+ return;
+ }
} else if (available_qty < qty_needed) {
frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
@@ -675,8 +681,8 @@ erpnext.PointOfSale.Controller = class {
},
callback(res) {
if (!me.item_stock_map[item_code])
- me.item_stock_map[item_code] = {}
- me.item_stock_map[item_code][warehouse] = res.message;
+ me.item_stock_map[item_code] = {};
+ me.item_stock_map[item_code][warehouse] = res.message[0];
}
});
}
@@ -707,4 +713,9 @@ erpnext.PointOfSale.Controller = class {
})
.catch(e => console.log(e));
}
+
+ async save_and_checkout() {
+ this.frm.is_dirty() && await this.frm.save();
+ this.payment.checkout();
+ }
};
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 4920584d95..4a99f068cd 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -191,10 +191,10 @@ erpnext.PointOfSale.ItemCart = class {
this.numpad_value = '';
});
- this.$component.on('click', '.checkout-btn', function() {
+ this.$component.on('click', '.checkout-btn', async function() {
if ($(this).attr('style').indexOf('--blue-500') == -1) return;
- me.events.checkout();
+ await me.events.checkout();
me.toggle_checkout_btn(false);
me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
@@ -985,6 +985,7 @@ erpnext.PointOfSale.ItemCart = class {
$(frm.wrapper).off('refresh-fields');
$(frm.wrapper).on('refresh-fields', () => {
if (frm.doc.items.length) {
+ this.$cart_items_wrapper.html('');
frm.doc.items.forEach(item => {
this.update_item_html(item);
});
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index a30bcd7cf6..1177615aee 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -79,14 +79,20 @@ erpnext.PointOfSale.ItemSelector = class {
const me = this;
// eslint-disable-next-line no-unused-vars
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
- const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
-
+ let indicator_color;
let qty_to_display = actual_qty;
- if (Math.round(qty_to_display) > 999) {
- qty_to_display = Math.round(qty_to_display)/1000;
- qty_to_display = qty_to_display.toFixed(1) + 'K';
+ if (item.is_stock_item) {
+ indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange");
+
+ if (Math.round(qty_to_display) > 999) {
+ qty_to_display = Math.round(qty_to_display)/1000;
+ qty_to_display = qty_to_display.toFixed(1) + 'K';
+ }
+ } else {
+ indicator_color = '';
+ qty_to_display = '';
}
function get_item_image_html() {
diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
index 777b02ca66..dd49f1355d 100644
--- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
+++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
@@ -23,19 +23,24 @@ def execute(filters=None):
row = []
outstanding_amt = get_customer_outstanding(d.name, filters.get("company"),
- ignore_outstanding_sales_order=d.bypass_credit_limit_check_at_sales_order)
+ ignore_outstanding_sales_order=d.bypass_credit_limit_check)
credit_limit = get_credit_limit(d.name, filters.get("company"))
bal = flt(credit_limit) - flt(outstanding_amt)
if customer_naming_type == "Naming Series":
- row = [d.name, d.customer_name, credit_limit, outstanding_amt, bal,
- d.bypass_credit_limit_check, d.is_frozen,
- d.disabled]
+ row = [
+ d.name, d.customer_name, credit_limit,
+ outstanding_amt, bal, d.bypass_credit_limit_check,
+ d.is_frozen, d.disabled
+ ]
else:
- row = [d.name, credit_limit, outstanding_amt, bal,
- d.bypass_credit_limit_check_at_sales_order, d.is_frozen, d.disabled]
+ row = [
+ d.name, credit_limit, outstanding_amt, bal,
+ d.bypass_credit_limit_check, d.is_frozen,
+ d.disabled
+ ]
if credit_limit:
data.append(row)
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index b2bf5464b5..3e22d0fa8c 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -85,7 +85,7 @@ def get_data(conditions, filters):
and so.docstatus = 1
{conditions}
GROUP BY soi.name
- ORDER BY so.transaction_date ASC
+ ORDER BY so.transaction_date ASC, soi.item_code ASC
""".format(conditions=conditions), filters, as_dict=1)
return data
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 540aca234b..16e3847168 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -486,7 +486,7 @@ frappe.ui.form.on(cur_frm.doctype, {
"options": "Competitor Detail"
},
{
- "fieldtype": "Text",
+ "fieldtype": "Small Text",
"label": __("Detailed Reason"),
"fieldname": "detailed_reason"
},
@@ -499,7 +499,7 @@ frappe.ui.form.on(cur_frm.doctype, {
method: 'declare_enquiry_lost',
args: {
'lost_reasons_list': values.lost_reason,
- 'competitors': values.competitors,
+ 'competitors': values.competitors ? values.competitors : [],
'detailed_reason': values.detailed_reason
},
callback: function(r) {
diff --git a/erpnext/selling/workspace/retail/retail.json b/erpnext/selling/workspace/retail/retail.json
index a851ace738..5bce3ca648 100644
--- a/erpnext/selling/workspace/retail/retail.json
+++ b/erpnext/selling/workspace/retail/retail.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Point Of Sale\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings & Configurations\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loyalty Program\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening & Closing\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Point Of Sale\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings & Configurations\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loyalty Program\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening & Closing\",\"col\":4}}]",
"creation": "2020-03-02 17:18:32.505616",
"docstatus": 0,
"doctype": "Workspace",
@@ -14,7 +14,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Settings & Configurations",
- "link_count": 0,
+ "link_count": 2,
"onboard": 0,
"type": "Card Break"
},
@@ -44,7 +44,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Loyalty Program",
- "link_count": 0,
+ "link_count": 2,
"onboard": 0,
"type": "Card Break"
},
@@ -74,7 +74,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Opening & Closing",
- "link_count": 0,
+ "link_count": 2,
"onboard": 0,
"type": "Card Break"
},
@@ -101,7 +101,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.840989",
+ "modified": "2022-01-13 18:07:56.711095",
"modified_by": "Administrator",
"module": "Selling",
"name": "Retail",
@@ -110,7 +110,7 @@
"public": 1,
"restrict_to_domain": "Retail",
"roles": [],
- "sequence_id": 22,
+ "sequence_id": 22.0,
"shortcuts": [
{
"doc_view": "",
diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json
index db2e6bafd5..a700ad89a3 100644
--- a/erpnext/selling/workspace/selling/selling.json
+++ b/erpnext/selling/workspace/selling/selling.json
@@ -5,7 +5,7 @@
"label": "Sales Order Trends"
}
],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Selling\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Sales Order Trends\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Quick Access\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Order Analysis\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Selling\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items and Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Quick Access \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-01-28 11:49:12.092882",
"docstatus": 0,
"doctype": "Workspace",
@@ -562,7 +562,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.990703",
+ "modified": "2022-01-13 17:43:02.778627",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",
@@ -571,7 +571,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 23,
+ "sequence_id": 23.0,
"shortcuts": [
{
"color": "Grey",
diff --git a/erpnext/setup/doctype/brand/brand.json b/erpnext/setup/doctype/brand/brand.json
index a8f0674b1f..45b4db81f1 100644
--- a/erpnext/setup/doctype/brand/brand.json
+++ b/erpnext/setup/doctype/brand/brand.json
@@ -1,270 +1,111 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:brand",
- "beta": 0,
- "creation": "2013-02-22 01:27:54",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:brand",
+ "creation": "2013-02-22 01:27:54",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "brand",
+ "image",
+ "description",
+ "defaults",
+ "brand_defaults"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 1,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "brand",
- "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": "Brand Name",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "brand",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "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,
- "translatable": 0,
+ "allow_in_quick_entry": 1,
+ "fieldname": "brand",
+ "fieldtype": "Data",
+ "label": "Brand Name",
+ "oldfieldname": "brand",
+ "oldfieldtype": "Data",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "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": "Description",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "permlevel": 0,
- "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": "description",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
"width": "300px"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "defaults",
- "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": "Defaults",
- "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": "defaults",
+ "fieldtype": "Section Break",
+ "label": "Defaults"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "brand_defaults",
- "fieldtype": "Table",
- "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": "Brand Defaults",
- "length": 0,
- "no_copy": 0,
- "options": "Item Default",
- "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": "brand_defaults",
+ "fieldtype": "Table",
+ "label": "Brand Defaults",
+ "options": "Item Default"
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach Image",
+ "hidden": 1,
+ "label": "Image"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-certificate",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-10-23 23:18:06.067612",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "Brand",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-certificate",
+ "idx": 1,
+ "image_field": "image",
+ "links": [],
+ "modified": "2021-03-01 15:57:30.005783",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Brand",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 1,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Item Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "import": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Item Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Stock User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Purchase User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User"
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 1,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
-}
+ ],
+ "quick_entry": 1,
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC"
+}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 63d96bf85e..370a3278a0 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:company_name",
- "creation": "2013-04-10 08:35:39",
+ "creation": "2022-01-25 10:29:55.938239",
"description": "Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Organization.",
"doctype": "DocType",
"document_type": "Setup",
@@ -77,13 +77,13 @@
"default_finance_book",
"auto_accounting_for_stock_settings",
"enable_perpetual_inventory",
- "enable_perpetual_inventory_for_non_stock_items",
+ "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",
- "service_received_but_not_billed",
+ "default_provisional_account",
"expenses_included_in_valuation",
"fixed_asset_defaults",
"accumulated_depreciation_account",
@@ -684,20 +684,6 @@
"label": "Default Buying Terms",
"options": "Terms and Conditions"
},
- {
- "fieldname": "service_received_but_not_billed",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Service Received But Not Billed",
- "no_copy": 1,
- "options": "Account"
- },
- {
- "default": "0",
- "fieldname": "enable_perpetual_inventory_for_non_stock_items",
- "fieldtype": "Check",
- "label": "Enable Perpetual Inventory For Non Stock Items"
- },
{
"fieldname": "default_in_transit_warehouse",
"fieldtype": "Link",
@@ -741,6 +727,20 @@
"fieldname": "section_break_28",
"fieldtype": "Section Break",
"label": "Chart of Accounts"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_provisional_accounting_for_non_stock_items",
+ "fieldtype": "Check",
+ "label": "Enable Provisional Accounting For Non Stock Items"
+ },
+ {
+ "fieldname": "default_provisional_account",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Default Provisional Account",
+ "no_copy": 1,
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -748,7 +748,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2021-10-04 12:09:25.833133",
+ "modified": "2022-01-25 10:33:16.826067",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -809,5 +809,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 0a02bcd6cd..95b1e8b9c6 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -10,6 +10,7 @@ import frappe.defaults
from frappe import _
from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.utils import cint, formatdate, get_timestamp, today
from frappe.utils.nestedset import NestedSet
@@ -45,7 +46,7 @@ class Company(NestedSet):
self.validate_currency()
self.validate_coa_input()
self.validate_perpetual_inventory()
- self.validate_perpetual_inventory_for_non_stock_items()
+ self.validate_provisional_account_for_non_stock_items()
self.check_country_change()
self.check_parent_changed()
self.set_chart_of_accounts()
@@ -187,11 +188,14 @@ class Company(NestedSet):
frappe.msgprint(_("Set default inventory account for perpetual inventory"),
alert=True, indicator='orange')
- def validate_perpetual_inventory_for_non_stock_items(self):
+ def validate_provisional_account_for_non_stock_items(self):
if not self.get("__islocal"):
- if cint(self.enable_perpetual_inventory_for_non_stock_items) == 1 and not self.service_received_but_not_billed:
- frappe.throw(_("Set default {0} account for perpetual inventory for non stock items").format(
- frappe.bold('Service Received But Not Billed')))
+ if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account:
+ frappe.throw(_("Set default {0} account for non stock items").format(
+ frappe.bold('Provisional Account')))
+
+ make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden",
+ not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False)
def check_country_change(self):
frappe.flags.country_change = False
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index c94b3463fc..4f92240c84 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -1,21 +1,17 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# 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, cstr, nowdate
+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 six.moves.urllib.parse import quote
-from erpnext.shopping_cart.filters import ProductFiltersBuilder
-from erpnext.shopping_cart.product_info import set_product_info_for_website
-from erpnext.shopping_cart.product_query import ProductQuery
-from erpnext.utilities.product import get_qty_in_stock
+from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
class ItemGroup(NestedSet, WebsiteGenerator):
@@ -67,30 +63,11 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.delete_child_item_groups_key()
def get_context(self, context):
- context.show_search=True
- context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6
+ 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'
- if frappe.form_dict:
- search = frappe.form_dict.search
- field_filters = frappe.parse_json(frappe.form_dict.field_filters)
- attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
- start = frappe.parse_json(frappe.form_dict.start)
- else:
- search = None
- attribute_filters = None
- field_filters = {}
- start = 0
-
- if not field_filters:
- field_filters = {}
-
- # Ensure the query remains within current item group & sub group
- field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)]
-
- engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
-
filter_engine = ProductFiltersBuilder(self.name)
context.field_filters = filter_engine.get_field_filters()
@@ -114,15 +91,16 @@ class ItemGroup(NestedSet, WebsiteGenerator):
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.theme or "Light"
- values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre"
- values[f"slide_{index + 1}_primary_action_label"] = slide.label
+ 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.breadcrumbs = 0
+ 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
@@ -133,91 +111,24 @@ class ItemGroup(NestedSet, WebsiteGenerator):
from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults)
-@frappe.whitelist(allow_guest=True)
-def get_product_list_for_group(product_group=None, start=0, limit=10, search=None):
- if product_group:
- item_group = frappe.get_cached_doc('Item Group', product_group)
- if item_group.is_group:
- # return child item groups if the type is of "Is Group"
- return get_child_groups_for_list_in_html(item_group, start, limit, search)
+def get_child_groups_for_website(item_group_name, immediate=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
+ }
- child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group))
+ if immediate:
+ filters["parent_item_group"] = item_group_name
- # base query
- query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group,
- I.description, I.web_long_description as website_description, I.is_stock_item,
- case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock, I.website_warehouse,
- I.has_batch_no
- from `tabItem` I
- left join tabBin S on I.item_code = S.item_code and I.website_warehouse = S.warehouse
- where I.show_in_website = 1
- and I.disabled = 0
- and (I.end_of_life is null or I.end_of_life='0000-00-00' or I.end_of_life > %(today)s)
- and (I.variant_of = '' or I.variant_of is null)
- and (I.item_group in ({child_groups})
- or I.name in (select parent from `tabWebsite Item Group` where item_group in ({child_groups})))
- """.format(child_groups=child_groups)
- # search term condition
- if search:
- query += """ and (I.web_long_description like %(search)s
- or I.item_name like %(search)s
- or I.name like %(search)s)"""
- search = "%" + cstr(search) + "%"
-
- query += """order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit))
-
- data = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1)
- data = adjust_qty_for_expired_items(data)
-
- if cint(frappe.db.get_single_value("Shopping Cart Settings", "enabled")):
- for item in data:
- set_product_info_for_website(item)
-
- return data
-
-def get_child_groups_for_list_in_html(item_group, start, limit, search):
- search_filters = None
- if search_filters:
- search_filters = [
- dict(name = ('like', '%{}%'.format(search))),
- dict(description = ('like', '%{}%'.format(search)))
- ]
- data = frappe.db.get_all('Item Group',
- fields = ['name', 'route', 'description', 'image'],
- filters = dict(
- show_in_website = 1,
- parent_item_group = item_group.name,
- lft = ('>', item_group.lft),
- rgt = ('<', item_group.rgt),
- ),
- or_filters = search_filters,
- order_by = 'weightage desc, name asc',
- start = start,
- limit = limit
+ return frappe.get_all(
+ "Item Group",
+ filters=filters,
+ fields=["name", "route"]
)
- return data
-
-def adjust_qty_for_expired_items(data):
- adjusted_data = []
-
- for item in data:
- if item.get('has_batch_no') and item.get('website_warehouse'):
- stock_qty_dict = get_qty_in_stock(
- item.get('name'), 'website_warehouse', item.get('website_warehouse'))
- qty = stock_qty_dict.stock_qty[0][0] if stock_qty_dict.stock_qty else 0
- item['in_stock'] = 1 if qty else 0
- adjusted_data.append(item)
-
- return adjusted_data
-
-
-def get_child_groups(item_group_name):
- item_group = frappe.get_doc("Item Group", item_group_name)
- return frappe.db.sql("""select name
- from `tabItem Group` where lft>=%(lft)s and rgt<=%(rgt)s
- and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt})
-
def get_child_item_groups(item_group_name):
item_group = frappe.get_cached_value("Item Group",
item_group_name, ["lft", "rgt"], as_dict=1)
@@ -233,31 +144,33 @@ def get_item_for_list_in_html(context):
if (context.get("website_image") or "").startswith("files/"):
context["website_image"] = "/" + quote(context["website_image"])
- context["show_availability_status"] = cint(frappe.db.get_single_value('Products Settings',
+ context["show_availability_status"] = cint(frappe.db.get_single_value('E Commerce Settings',
'show_availability_status'))
products_template = 'templates/includes/products_as_list.html'
return frappe.get_template(products_template).render(context)
-def get_group_item_count(item_group):
- child_groups = ", ".join('"' + i[0] + '"' for i in get_child_groups(item_group))
- return frappe.db.sql("""select count(*) from `tabItem`
- where docstatus = 0 and show_in_website = 1
- and (item_group in (%s)
- or name in (select parent from `tabWebsite Item Group`
- where item_group in (%s))) """ % (child_groups, child_groups))[0][0]
+def get_parent_item_groups(item_group_name, from_item=False):
+ base_nav_page = {"name": _("Shop by Category"), "route":"/shop-by-category"}
+
+ 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]
+ 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}
-def get_parent_item_groups(item_group_name):
base_parents = [
- {"name": frappe._("Home"), "route":"/"},
- {"name": frappe._("All Products"), "route":"/all-products"},
+ {"name": _("Home"), "route":"/"},
+ base_nav_page,
]
+
if not item_group_name:
return base_parents
- item_group = frappe.get_doc("Item Group", item_group_name)
+ 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
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index bafaab814b..1d7bad2686 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -189,7 +189,7 @@ def add_non_standard_user_types():
user_type_limit = {}
for user_type, data in user_types.items():
- user_type_limit.setdefault(frappe.scrub(user_type), 10)
+ user_type_limit.setdefault(frappe.scrub(user_type), 20)
update_site_config('user_type_doctype_limit', user_type_limit)
@@ -204,15 +204,33 @@ def get_user_types_data():
'apply_user_permission_on': 'Employee',
'user_id_field': 'user_id',
'doctypes': {
- 'Salary Slip': ['read'],
+ # masters
+ 'Holiday List': ['read'],
'Employee': ['read', 'write'],
+ # payroll
+ 'Salary Slip': ['read'],
+ 'Employee Benefit Application': ['read', 'write', 'create', 'delete'],
+ # expenses
'Expense Claim': ['read', 'write', 'create', 'delete'],
+ 'Employee Advance': ['read', 'write', 'create', 'delete'],
+ # leave and attendance
'Leave Application': ['read', 'write', 'create', 'delete'],
'Attendance Request': ['read', 'write', 'create', 'delete'],
'Compensatory Leave Request': ['read', 'write', 'create', 'delete'],
+ # tax
'Employee Tax Exemption Declaration': ['read', 'write', 'create', 'delete'],
'Employee Tax Exemption Proof Submission': ['read', 'write', 'create', 'delete'],
- 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend']
+ # projects
+ 'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'],
+ # trainings
+ 'Training Program': ['read'],
+ 'Training Feedback': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'],
+ # shifts
+ 'Shift Request': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend'],
+ # misc
+ 'Employee Grievance': ['read', 'write', 'create', 'delete'],
+ 'Employee Referral': ['read', 'write', 'create', 'delete'],
+ 'Travel Request': ['read', 'write', 'create', 'delete']
}
}
}
diff --git a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json
deleted file mode 100644
index f00dc947d2..0000000000
--- a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "add_more_button": 0,
- "app": "ERPNext",
- "creation": "2019-12-04 19:21:39.995776",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:53:53.849953",
- "modified_by": "Administrator",
- "name": "Welcome back to ERPNext!",
- "owner": "Administrator",
- "slide_desc": "
Let's continue where you left from!
",
- "slide_fields": [],
- "slide_module": "Setup",
- "slide_order": 0,
- "slide_title": "Welcome back to ERPNext!",
- "slide_type": "Continue"
-}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json
deleted file mode 100644
index 37eb67b1d7..0000000000
--- a/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "add_more_button": 0,
- "app": "ERPNext",
- "creation": "2019-11-26 17:01:26.671859",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 0,
- "modified": "2019-12-22 21:26:28.414597",
- "modified_by": "Administrator",
- "name": "Welcome to ERPNext!",
- "owner": "Administrator",
- "slide_desc": "
Setting up an ERP can be overwhelming. But don't worry, we have got your back! This wizard will help you onboard to ERPNext in a short time!
",
- "slide_fields": [],
- "slide_module": "Setup",
- "slide_order": 1,
- "slide_title": "Welcome to ERPNext!",
- "slide_type": "Information"
-}
\ No newline at end of file
diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py
index 358b921831..74c1bd835d 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -29,10 +29,10 @@ def create_fiscal_year_and_company(args):
'domain': args.get('domains')[0]
}).insert()
-def enable_shopping_cart(args):
+def enable_shopping_cart(args): # nosemgrep
# Needs price_lists
frappe.get_doc({
- "doctype": "Shopping Cart Settings",
+ "doctype": "E Commerce Settings",
"enabled": 1,
'company': args.get('company_name') ,
'price_list': frappe.db.get_value("Price List", {"selling": 1}),
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 9dbf49eae7..cd2738aeaa 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -535,8 +535,8 @@ def create_bank_account(args):
# bank account same as a CoA entry
pass
-def update_shopping_cart_settings(args):
- shopping_cart = frappe.get_doc("Shopping Cart Settings")
+def update_shopping_cart_settings(args): # nosemgrep
+ shopping_cart = frappe.get_doc("E Commerce Settings")
shopping_cart.update({
"enabled": 1,
'company': args.company_name,
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index e47837f2ca..c5640bc079 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"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\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":4}}]",
+ "content": "[{\"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}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Manufacturing Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Education Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Hotel Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Settings\",\"col\":3}}]",
"creation": "2020-03-12 14:47:51.166455",
"docstatus": 0,
"doctype": "Workspace",
@@ -10,7 +10,7 @@
"idx": 0,
"label": "ERPNext Settings",
"links": [],
- "modified": "2021-11-05 21:32:55.323591",
+ "modified": "2022-01-13 19:18:59.362820",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -19,7 +19,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 12,
+ "sequence_id": 12.0,
"shortcuts": [
{
"icon": "project",
@@ -104,13 +104,6 @@
"restrict_to_domain": "Hospitality",
"type": "DocType"
},
- {
- "icon": "non-profit",
- "label": "Healthcare Settings",
- "link_to": "Healthcare Settings",
- "restrict_to_domain": "Healthcare",
- "type": "DocType"
- },
{
"icon": "setting",
"label": "Domain Settings",
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index f9c585c015..19ff2a0830 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -1,18 +1,13 @@
{
"charts": [],
- "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"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\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
"creation": "2020-01-23 13:46:38.833076",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "getting-started",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Home",
"links": [
{
@@ -276,18 +271,16 @@
"type": "Link"
}
],
- "modified": "2021-11-22 12:50:15.771366",
+ "modified": "2022-01-13 17:24:17.002665",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 1,
+ "sequence_id": 1.0,
"shortcuts": [
{
"label": "Item",
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
deleted file mode 100644
index 7a4bb20136..0000000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
+++ /dev/null
@@ -1,212 +0,0 @@
-{
- "actions": [],
- "creation": "2013-06-19 15:57:32",
- "description": "Default settings for Shopping Cart",
- "doctype": "DocType",
- "document_type": "System",
- "engine": "InnoDB",
- "field_order": [
- "enabled",
- "store_page_docs",
- "display_settings",
- "show_attachments",
- "show_price",
- "show_stock_availability",
- "enable_variants",
- "column_break_7",
- "show_contact_us_button",
- "show_quantity_in_website",
- "show_apply_coupon_code_in_website",
- "allow_items_not_in_stock",
- "section_break_2",
- "company",
- "price_list",
- "column_break_4",
- "default_customer_group",
- "quotation_series",
- "section_break_8",
- "enable_checkout",
- "save_quotations_as_draft",
- "column_break_11",
- "payment_gateway_account",
- "payment_success_url"
- ],
- "fields": [
- {
- "default": "0",
- "fieldname": "enabled",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Enable Shopping Cart"
- },
- {
- "fieldname": "display_settings",
- "fieldtype": "Section Break",
- "label": "Display Settings"
- },
- {
- "default": "0",
- "fieldname": "show_attachments",
- "fieldtype": "Check",
- "label": "Show Public Attachments"
- },
- {
- "default": "0",
- "fieldname": "show_price",
- "fieldtype": "Check",
- "label": "Show Price"
- },
- {
- "default": "0",
- "fieldname": "show_stock_availability",
- "fieldtype": "Check",
- "label": "Show Stock Availability"
- },
- {
- "default": "0",
- "fieldname": "show_contact_us_button",
- "fieldtype": "Check",
- "label": "Show Contact Us Button"
- },
- {
- "default": "0",
- "depends_on": "show_stock_availability",
- "fieldname": "show_quantity_in_website",
- "fieldtype": "Check",
- "label": "Show Stock Quantity"
- },
- {
- "default": "0",
- "fieldname": "show_apply_coupon_code_in_website",
- "fieldtype": "Check",
- "label": "Show Apply Coupon Code"
- },
- {
- "default": "0",
- "fieldname": "allow_items_not_in_stock",
- "fieldtype": "Check",
- "label": "Allow items not in stock to be added to cart"
- },
- {
- "depends_on": "enabled",
- "fieldname": "section_break_2",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Company",
- "mandatory_depends_on": "eval: doc.enabled === 1",
- "options": "Company",
- "remember_last_selected_value": 1
- },
- {
- "description": "Prices will not be shown if Price List is not set",
- "fieldname": "price_list",
- "fieldtype": "Link",
- "label": "Price List",
- "mandatory_depends_on": "eval: doc.enabled === 1",
- "options": "Price List"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "default_customer_group",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Default Customer Group",
- "mandatory_depends_on": "eval: doc.enabled === 1",
- "options": "Customer Group"
- },
- {
- "fieldname": "quotation_series",
- "fieldtype": "Select",
- "label": "Quotation Series",
- "mandatory_depends_on": "eval: doc.enabled === 1"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "eval:doc.enable_checkout",
- "depends_on": "enabled",
- "fieldname": "section_break_8",
- "fieldtype": "Section Break",
- "label": "Checkout Settings"
- },
- {
- "default": "0",
- "fieldname": "enable_checkout",
- "fieldtype": "Check",
- "label": "Enable Checkout"
- },
- {
- "default": "Orders",
- "depends_on": "enable_checkout",
- "description": "After payment completion redirect user to selected page.",
- "fieldname": "payment_success_url",
- "fieldtype": "Select",
- "label": "Payment Success Url",
- "mandatory_depends_on": "enable_checkout",
- "options": "\nOrders\nInvoices\nMy Account"
- },
- {
- "fieldname": "column_break_11",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "enable_checkout",
- "fieldname": "payment_gateway_account",
- "fieldtype": "Link",
- "label": "Payment Gateway Account",
- "mandatory_depends_on": "enable_checkout",
- "options": "Payment Gateway Account"
- },
- {
- "fieldname": "column_break_7",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "enable_variants",
- "fieldtype": "Check",
- "label": "Enable Variants"
- },
- {
- "default": "0",
- "depends_on": "eval: doc.enable_checkout == 0",
- "fieldname": "save_quotations_as_draft",
- "fieldtype": "Check",
- "label": "Save Quotations as Draft"
- },
- {
- "depends_on": "doc.enabled",
- "fieldname": "store_page_docs",
- "fieldtype": "HTML"
- }
- ],
- "icon": "fa fa-shopping-cart",
- "idx": 1,
- "issingle": 1,
- "links": [],
- "modified": "2021-03-02 17:34:57.642565",
- "modified_by": "Administrator",
- "module": "Shopping Cart",
- "name": "Shopping Cart Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Website Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py
deleted file mode 100644
index ef0badc8c8..0000000000
--- a/erpnext/shopping_cart/filters.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-class ProductFiltersBuilder:
- def __init__(self, item_group=None):
- if not item_group or item_group == "Products Settings":
- self.doc = frappe.get_doc("Products Settings")
- else:
- self.doc = frappe.get_doc("Item Group", item_group)
-
- self.item_group = item_group
-
- def get_field_filters(self):
- filter_fields = [row.fieldname for row in self.doc.filter_fields]
-
- meta = frappe.get_meta('Item')
- fields = [df for df in meta.fields if df.fieldname in filter_fields]
-
- filter_data = []
- for df in fields:
- filters, or_filters = {}, []
- if df.fieldtype == "Link":
- if self.item_group:
- or_filters.extend([
- ["item_group", "=", self.item_group],
- ["Website Item Group", "item_group", "=", self.item_group]
- ])
-
- values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
- else:
- doctype = df.get_link_doctype()
-
- # apply enable/disable/show_in_website filter
- meta = frappe.get_meta(doctype)
-
- if meta.has_field('enabled'):
- filters['enabled'] = 1
- if meta.has_field('disabled'):
- filters['disabled'] = 0
- if meta.has_field('show_in_website'):
- filters['show_in_website'] = 1
-
- values = [d.name for d in frappe.get_all(doctype, filters)]
-
- # Remove None
- if None in values:
- values.remove(None)
-
- if values:
- filter_data.append([df, values])
-
- return filter_data
-
- def get_attribute_filters(self):
- attributes = [row.attribute for row in self.doc.filter_attributes]
-
- if not attributes:
- return []
-
- result = frappe.db.sql(
- """
- select
- distinct attribute, attribute_value
- from
- `tabItem Variant Attribute`
- where
- attribute in %(attributes)s
- and attribute_value is not null
- """,
- {"attributes": attributes},
- as_dict=1,
- )
-
- attribute_value_map = {}
- for d in result:
- attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
-
- out = []
- for name, values in attribute_value_map.items():
- out.append(frappe._dict(name=name, item_attribute_values=values))
- return out
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
deleted file mode 100644
index 5cc0505aed..0000000000
--- a/erpnext/shopping_cart/product_query.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-
-from erpnext.shopping_cart.product_info import get_product_info_for_website
-
-
-class ProductQuery:
- """Query engine for product listing
-
- Attributes:
- cart_settings (Document): Settings for Cart
- fields (list): Fields to fetch in query
- filters (TYPE): Description
- or_filters (list): Description
- page_length (Int): Length of page for the query
- settings (Document): Products Settings DocType
- filters (list)
- or_filters (list)
- """
-
- def __init__(self):
- self.settings = frappe.get_doc("Products Settings")
- self.cart_settings = frappe.get_doc("Shopping Cart Settings")
- self.page_length = self.settings.products_per_page or 20
- self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
- 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
- self.filters = []
- self.or_filters = [['show_in_website', '=', 1]]
- if not self.settings.get('hide_variants'):
- self.or_filters.append(['show_variant_in_website', '=', 1])
-
- def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
- """Summary
-
- Args:
- attributes (dict, optional): Item Attribute filters
- fields (dict, optional): Field level filters
- search_term (str, optional): Search term to lookup
- start (int, optional): Page start
-
- Returns:
- list: List of results with set fields
- """
- if fields: self.build_fields_filters(fields)
- if search_term: self.build_search_filters(search_term)
-
- result = []
- website_item_groups = []
-
- # if from item group page consider website item group table
- if item_group:
- website_item_groups = frappe.db.get_all(
- "Item",
- fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
- filters=[["Website Item Group", "item_group", "=", item_group]]
- )
-
- if attributes:
- all_items = []
- for attribute, values in attributes.items():
- if not isinstance(values, list):
- values = [values]
-
- items = frappe.get_all(
- "Item",
- fields=self.fields,
- filters=[
- *self.filters,
- ["Item Variant Attribute", "attribute", "=", attribute],
- ["Item Variant Attribute", "attribute_value", "in", values],
- ],
- or_filters=self.or_filters,
- start=start,
- limit=self.page_length,
- order_by="weightage desc"
- )
-
- items_dict = {item.name: item for item in items}
-
- all_items.append(set(items_dict.keys()))
-
- result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
- else:
- result = frappe.get_all(
- "Item",
- fields=self.fields,
- filters=self.filters,
- or_filters=self.or_filters,
- start=start,
- limit=self.page_length,
- order_by="weightage desc"
- )
-
- # Combine results having context of website item groups into item results
- if item_group and website_item_groups:
- items_list = {row.name for row in result}
- for row in website_item_groups:
- if row.wig_parent not in items_list:
- result.append(row)
-
- result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
-
- for item in result:
- product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
- if product_info:
- item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
-
- return result
-
- def build_fields_filters(self, filters):
- """Build filters for field values
-
- Args:
- filters (dict): Filters
- """
- for field, values in filters.items():
- if not values:
- continue
-
- # handle multiselect fields in filter addition
- meta = frappe.get_meta('Item', cached=True)
- df = meta.get_field(field)
- if df.fieldtype == 'Table MultiSelect':
- child_doctype = df.options
- child_meta = frappe.get_meta(child_doctype, cached=True)
- fields = child_meta.get("fields")
- if fields:
- self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
- elif isinstance(values, list):
- # If value is a list use `IN` query
- self.filters.append([field, 'IN', values])
- else:
- # `=` will be faster than `IN` for most cases
- self.filters.append([field, '=', values])
-
- def build_search_filters(self, search_term):
- """Query search term in specified fields
-
- Args:
- search_term (str): Search candidate
- """
- # Default fields to search from
- default_fields = {'name', 'item_name', 'description', 'item_group'}
-
- # Get meta search fields
- meta = frappe.get_meta("Item")
- meta_fields = set(meta.get_search_fields())
-
- # Join the meta fields and default fields set
- search_fields = default_fields.union(meta_fields)
- try:
- if frappe.db.count('Item', cache=True) > 50000:
- search_fields.remove('description')
- except KeyError:
- pass
-
- # Build or filters for query
- search = '%{}%'.format(search_term)
- self.or_filters += [[field, 'like', search] for field in search_fields]
diff --git a/erpnext/shopping_cart/web_template/hero_slider/__init__.py b/erpnext/shopping_cart/web_template/hero_slider/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/shopping_cart/web_template/item_card_group/__init__.py b/erpnext/shopping_cart/web_template/item_card_group/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/shopping_cart/web_template/product_card/__init__.py b/erpnext/shopping_cart/web_template/product_card/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/__init__.py b/erpnext/shopping_cart/web_template/product_category_cards/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 5593101575..96751d6eae 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -292,6 +292,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
join `tabStock Ledger Entry` ignore index (item_code, warehouse)
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
+ and `tabStock Ledger Entry`.is_cancelled = 0
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
@@ -336,4 +337,4 @@ def get_pos_reserved_batch_qty(filters):
).run()
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
- return flt_reserved_batch_qty
\ No newline at end of file
+ return flt_reserved_batch_qty
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 8e79f0e555..56dc71c57e 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -33,6 +33,7 @@
"oldfieldtype": "Link",
"options": "Warehouse",
"read_only": 1,
+ "reqd": 1,
"search_index": 1
},
{
@@ -46,6 +47,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
+ "reqd": 1,
"search_index": 1
},
{
@@ -169,10 +171,11 @@
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2021-03-30 23:09:39.572776",
+ "modified": "2022-01-30 17:04:54.715288",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -200,5 +203,6 @@
"quick_entry": 1,
"search_fields": "item_code,warehouse",
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 0ef7ce2923..c34e9d05ce 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -123,7 +123,7 @@ class Bin(Document):
self.db_set('projected_qty', self.projected_qty)
def on_doctype_update():
- frappe.db.add_index("Bin", ["item_code", "warehouse"])
+ frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py
index 9c390d94b4..250126c6b9 100644
--- a/erpnext/stock/doctype/bin/test_bin.py
+++ b/erpnext/stock/doctype/bin/test_bin.py
@@ -1,9 +1,36 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
+import frappe
-# test_records = frappe.get_test_records('Bin')
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.utils import _create_bin
+from erpnext.tests.utils import ERPNextTestCase
-class TestBin(unittest.TestCase):
- pass
+
+class TestBin(ERPNextTestCase):
+
+
+ def test_concurrent_inserts(self):
+ """ Ensure no duplicates are possible in case of concurrent inserts"""
+ item_code = "_TestConcurrentBin"
+ make_item(item_code)
+ warehouse = "_Test Warehouse - _TC"
+
+ bin1 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
+ bin1.insert()
+
+ bin2 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
+ with self.assertRaises(frappe.UniqueValidationError):
+ bin2.insert()
+
+ # util method should handle it
+ bin = _create_bin(item_code, warehouse)
+ self.assertEqual(bin.item_code, item_code)
+
+ frappe.db.rollback()
+
+ def test_index_exists(self):
+ indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
+ if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
+ self.fail(f"Expected unique index on item-warehouse")
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 70d48a42d7..d1e22440b9 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -14,6 +14,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
+from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -128,8 +129,12 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc()
- from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
- make_packing_list(self)
+ # Keeps mapped packed_items in case product bundle is updated.
+ if self.is_return and self.return_against:
+ calculate_mapped_packed_items_return(self)
+ else:
+ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+ make_packing_list(self)
if self._action != 'submit' and not self.is_return:
set_batch_nos(self, 'warehouse', throw=True)
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 4f89a19f3c..bd18e788ba 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -386,8 +386,7 @@ class TestDeliveryNote(ERPNextTestCase):
self.assertEqual(actual_qty, 25)
# return bundled item
- dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1,
- return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
+ dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2)
# qty after return
actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1")
@@ -823,6 +822,15 @@ class TestDeliveryNote(ERPNextTestCase):
automatically_fetch_payment_terms(enable=0)
+def create_return_delivery_note(**args):
+ args = frappe._dict(args)
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ doc = make_return_doc("Delivery Note", args.source_name, None)
+ doc.items[0].rate = args.rate
+ doc.items[0].qty = args.qty
+ doc.submit()
+ return doc
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 752a1fe732..2a30ca11fb 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -17,8 +17,6 @@ frappe.ui.form.on("Item", {
frm.fields_dict["attributes"].grid.set_column_disp("attribute_value", true);
}
- // should never check Private
- frm.fields_dict["website_image"].df.is_private = 0;
if (frm.doc.is_fixed_asset) {
frm.trigger("set_asset_naming_series");
}
@@ -91,6 +89,29 @@ 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'));
+ }
+
erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
@@ -182,25 +203,8 @@ frappe.ui.form.on("Item", {
}
},
- copy_from_item_group: function(frm) {
- return frm.call({
- doc: frm.doc,
- method: "copy_specification_from_item_group"
- });
- },
-
has_variants: function(frm) {
erpnext.item.toggle_attributes(frm);
- },
-
- show_in_website: function(frm) {
- if (frm.doc.default_warehouse && !frm.doc.website_warehouse){
- frm.set_value("website_warehouse", frm.doc.default_warehouse);
- }
- },
-
- set_meta_tags(frm) {
- frappe.utils.set_meta_tag(frm.doc.route);
}
});
@@ -376,8 +380,7 @@ $.extend(erpnext.item, {
// Show Stock Levels only if is_stock_item
if (frm.doc.is_stock_item) {
frappe.require('item-dashboard.bundle.js', function() {
- frm.dashboard.parent.find('.stock-levels').remove();
- const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels');
+ const section = frm.dashboard.add_section('', __("Stock Levels"));
erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
parent: section,
item_code: frm.doc.name,
@@ -393,13 +396,15 @@ $.extend(erpnext.item, {
edit_prices_button: function(frm) {
frm.add_custom_button(__("Add / Edit Prices"), function() {
frappe.set_route("List", "Item Price", {"item_code": frm.doc.name});
- }, __("View"));
+ }, __("Actions"));
},
- weight_to_validate: function(frm){
- if((frm.doc.nett_weight || frm.doc.gross_weight) && !frm.doc.weight_uom) {
- frappe.msgprint(__('Weight is mentioned,\nPlease mention "Weight UOM" too'));
- frappe.validated = 0;
+ weight_to_validate: function(frm) {
+ if (frm.doc.weight_per_unit && !frm.doc.weight_uom) {
+ frappe.msgprint({
+ message: __("Please mention 'Weight UOM' along with Weight."),
+ title: __("Note")
+ });
}
},
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 29abd45fcc..e71cdb37cf 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -28,6 +28,7 @@
"standard_rate",
"is_fixed_asset",
"auto_create_assets",
+ "is_grouped_asset",
"asset_category",
"asset_naming_series",
"over_delivery_receipt_allowance",
@@ -116,24 +117,8 @@
"customer_code",
"default_item_manufacturer",
"default_manufacturer_part_no",
- "website_section",
- "show_in_website",
- "show_variant_in_website",
- "route",
- "weightage",
- "slideshow",
- "website_image",
- "website_image_alt",
- "thumbnail",
- "cb72",
- "website_warehouse",
- "website_item_groups",
- "set_meta_tags",
- "sb72",
- "copy_from_item_group",
- "website_specifications",
- "web_long_description",
- "website_content",
+ "more_information_section",
+ "published_in_website",
"total_projected_qty"
],
"fields": [
@@ -855,125 +840,6 @@
"no_copy": 1,
"print_hide": 1
},
- {
- "collapsible": 1,
- "depends_on": "eval:!doc.is_fixed_asset",
- "fieldname": "website_section",
- "fieldtype": "Section Break",
- "label": "Website",
- "options": "fa fa-globe"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.variant_of",
- "fieldname": "show_in_website",
- "fieldtype": "Check",
- "label": "Show in Website",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "variant_of",
- "fieldname": "show_variant_in_website",
- "fieldtype": "Check",
- "label": "Show in Website (Variant)",
- "search_index": 1
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "route",
- "fieldtype": "Small Text",
- "label": "Route",
- "no_copy": 1
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Items with higher weightage will be shown higher",
- "fieldname": "weightage",
- "fieldtype": "Int",
- "label": "Weightage"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Show a slideshow at the top of the page",
- "fieldname": "slideshow",
- "fieldtype": "Link",
- "label": "Slideshow",
- "options": "Website Slideshow"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Item Image (if not slideshow)",
- "fieldname": "website_image",
- "fieldtype": "Attach",
- "label": "Website Image"
- },
- {
- "fieldname": "thumbnail",
- "fieldtype": "Data",
- "label": "Thumbnail",
- "read_only": 1
- },
- {
- "fieldname": "cb72",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.",
- "fieldname": "website_warehouse",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Website Warehouse",
- "options": "Warehouse"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "List this Item in multiple groups on the website.",
- "fieldname": "website_item_groups",
- "fieldtype": "Table",
- "label": "Website Item Groups",
- "options": "Website Item Group"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "set_meta_tags",
- "fieldtype": "Button",
- "label": "Set Meta Tags"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "website_specifications",
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "sb72",
- "fieldtype": "Section Break",
- "label": "Website Specifications"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "copy_from_item_group",
- "fieldtype": "Button",
- "label": "Copy From Item Group"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "website_specifications",
- "fieldtype": "Table",
- "label": "Website Specifications",
- "options": "Item Website Specification"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "web_long_description",
- "fieldtype": "Text Editor",
- "label": "Website Description"
- },
- {
- "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.",
- "fieldname": "website_content",
- "fieldtype": "HTML Editor",
- "label": "Website Content"
- },
{
"fieldname": "total_projected_qty",
"fieldtype": "Float",
@@ -1016,25 +882,39 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "website_image_alt",
- "fieldtype": "Data",
- "label": "Image Description"
+ "collapsible": 1,
+ "fieldname": "more_information_section",
+ "fieldtype": "Section Break",
+ "label": "More Information"
+ },
+ {
+ "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",
"fieldtype": "Check",
"label": "Grant Commission"
+ },
+ {
+ "default": "0",
+ "depends_on": "auto_create_assets",
+ "fieldname": "is_grouped_asset",
+ "fieldtype": "Check",
+ "label": "Create Grouped Asset"
}
],
- "has_web_view": 1,
"icon": "fa fa-tag",
"idx": 2,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-12-14 04:13:16.857534",
+ "modified": "2022-01-18 12:57:54.273202",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -1104,6 +984,7 @@
"show_preview_popup": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "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 281e881875..b9e8b3f2f1 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -2,12 +2,12 @@
# License: GNU General Public License v3. See license.txt
import copy
-import itertools
import json
from typing import List
import frappe
from frappe import _
+from frappe.model.document import Document
from frappe.utils import (
cint,
cstr,
@@ -17,13 +17,9 @@ from frappe.utils import (
getdate,
now_datetime,
nowtime,
- random_string,
strip,
)
from frappe.utils.html_utils import clean_html
-from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
-from frappe.website.utils import clear_cache
-from frappe.website.website_generator import WebsiteGenerator
import erpnext
from erpnext.controllers.item_variant import (
@@ -33,10 +29,7 @@ from erpnext.controllers.item_variant import (
make_variant_item_code,
validate_item_variant_attributes,
)
-from erpnext.setup.doctype.item_group.item_group import (
- get_parent_item_groups,
- invalidate_cache_for,
-)
+from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
from erpnext.stock.doctype.item_default.item_default import ItemDefault
@@ -51,18 +44,11 @@ class StockExistsForTemplate(frappe.ValidationError):
class InvalidBarcode(frappe.ValidationError):
pass
+class DataValidationError(frappe.ValidationError):
+ pass
-class Item(WebsiteGenerator):
- website = frappe._dict(
- page_title_field="item_name",
- condition_field="show_in_website",
- template="templates/generators/item/item.html",
- no_cache=1
- )
-
+class Item(Document):
def onload(self):
- super(Item, self).onload()
-
self.set_onload('stock_exists', self.stock_ledger_created())
self.set_asset_naming_series()
@@ -103,8 +89,6 @@ class Item(WebsiteGenerator):
self.set_opening_stock()
def validate(self):
- super(Item, self).validate()
-
if not self.item_name:
self.item_name = self.item_code
@@ -130,8 +114,6 @@ class Item(WebsiteGenerator):
self.validate_attributes()
self.validate_variant_attributes()
self.validate_variant_based_on_change()
- self.validate_website_image()
- self.make_thumbnail()
self.validate_fixed_asset()
self.validate_retain_sample()
self.validate_uom_conversion_factor()
@@ -140,21 +122,17 @@ class Item(WebsiteGenerator):
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
- self.update_show_in_website()
self.validate_item_tax_net_rate_range()
set_item_tax_from_hsn_code(self)
if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
- self.old_website_item_groups = frappe.db.sql_list("""select item_group
- from `tabWebsite Item Group`
- where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name)
def on_update(self):
invalidate_cache_for_item(self)
self.update_variants()
self.update_item_price()
- self.update_template_item()
+ self.update_website_item()
def validate_description(self):
'''Clean HTML description if set'''
@@ -216,97 +194,6 @@ class Item(WebsiteGenerator):
stock_entry.add_comment("Comment", _("Opening Stock"))
- def make_route(self):
- if not self.route:
- return cstr(frappe.db.get_value('Item Group', self.item_group,
- 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
-
- def validate_website_image(self):
- """Validate if the website image is a public file"""
-
- if frappe.flags.in_import:
- return
-
- auto_set_website_image = False
- if not self.website_image and self.image:
- auto_set_website_image = True
- self.website_image = self.image
-
- if not self.website_image:
- return
-
- # find if website image url exists as public
- file_doc = frappe.get_all("File", filters={
- "file_url": self.website_image
- }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1)
-
- if file_doc:
- file_doc = file_doc[0]
-
- if not file_doc:
- if not auto_set_website_image:
- frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
-
- self.website_image = None
-
- elif file_doc.is_private:
- if not auto_set_website_image:
- frappe.msgprint(_("Website Image should be a public file or website URL"))
-
- self.website_image = None
-
- def make_thumbnail(self):
- """Make a thumbnail of `website_image`"""
-
- if frappe.flags.in_import:
- return
-
- import requests.exceptions
-
- if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
- self.thumbnail = None
-
- if self.website_image and not self.thumbnail:
- file_doc = None
-
- try:
- file_doc = frappe.get_doc("File", {
- "file_url": self.website_image,
- "attached_to_doctype": "Item",
- "attached_to_name": self.name
- })
- except frappe.DoesNotExistError:
- # cleanup
- frappe.local.message_log.pop()
-
- except requests.exceptions.HTTPError:
- frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
- self.website_image = None
-
- except requests.exceptions.SSLError:
- frappe.msgprint(
- _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
- self.website_image = None
-
- # for CSV import
- if self.website_image and not file_doc:
- try:
- file_doc = frappe.get_doc({
- "doctype": "File",
- "file_url": self.website_image,
- "attached_to_doctype": "Item",
- "attached_to_name": self.name
- }).save()
-
- except IOError:
- self.website_image = None
-
- if file_doc:
- if not file_doc.thumbnail_url:
- file_doc.make_thumbnail()
-
- self.thumbnail = file_doc.thumbnail_url
-
def validate_fixed_asset(self):
if self.is_fixed_asset:
if self.is_stock_item:
@@ -330,167 +217,6 @@ class Item(WebsiteGenerator):
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code))
- def get_context(self, context):
- context.show_search = True
- context.search_link = '/product_search'
-
- context.parents = get_parent_item_groups(self.item_group)
- context.body_class = "product-page"
-
- self.set_variant_context(context)
- self.set_attribute_context(context)
- self.set_disabled_attributes(context)
- self.set_metatags(context)
- self.set_shopping_cart_data(context)
-
- return context
-
- def set_variant_context(self, context):
- if self.has_variants:
- context.no_cache = True
-
- # load variants
- # also used in set_attribute_context
- context.variants = frappe.get_all("Item",
- filters={"variant_of": self.name, "show_variant_in_website": 1},
- order_by="name asc")
-
- variant = frappe.form_dict.variant
- if not variant and context.variants:
- # the case when the item is opened for the first time from its list
- variant = context.variants[0]
-
- if variant:
- context.variant = frappe.get_doc("Item", variant)
-
- for fieldname in ("website_image", "website_image_alt", "web_long_description", "description",
- "website_specifications"):
- if context.variant.get(fieldname):
- value = context.variant.get(fieldname)
- if isinstance(value, list):
- value = [d.as_dict() for d in value]
-
- context[fieldname] = value
-
- if self.slideshow:
- if context.variant and context.variant.slideshow:
- context.update(get_slideshow(context.variant))
- else:
- context.update(get_slideshow(self))
-
- def set_attribute_context(self, context):
- if not self.has_variants:
- return
-
- attribute_values_available = {}
- context.attribute_values = {}
- context.selected_attributes = {}
-
- # load attributes
- for v in context.variants:
- v.attributes = frappe.get_all("Item Variant Attribute",
- fields=["attribute", "attribute_value"],
- filters={"parent": v.name})
- # make a map for easier access in templates
- v.attribute_map = frappe._dict({})
- for attr in v.attributes:
- v.attribute_map[attr.attribute] = attr.attribute_value
-
- for attr in v.attributes:
- values = attribute_values_available.setdefault(attr.attribute, [])
- if attr.attribute_value not in values:
- values.append(attr.attribute_value)
-
- if v.name == context.variant.name:
- context.selected_attributes[attr.attribute] = attr.attribute_value
-
- # filter attributes, order based on attribute table
- for attr in self.attributes:
- values = context.attribute_values.setdefault(attr.attribute, [])
-
- if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
- for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
- values.append(val)
-
- else:
- # get list of values defined (for sequence)
- for attr_value in frappe.db.get_all("Item Attribute Value",
- fields=["attribute_value"],
- filters={"parent": attr.attribute}, order_by="idx asc"):
-
- if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
- values.append(attr_value.attribute_value)
-
- context.variant_info = json.dumps(context.variants)
-
- def set_disabled_attributes(self, context):
- """Disable selection options of attribute combinations that do not result in a variant"""
- if not self.attributes or not self.has_variants:
- return
-
- context.disabled_attributes = {}
- attributes = [attr.attribute for attr in self.attributes]
-
- def find_variant(combination):
- for variant in context.variants:
- if len(variant.attributes) < len(attributes):
- continue
-
- if "combination" not in variant:
- ref_combination = []
-
- for attr in variant.attributes:
- idx = attributes.index(attr.attribute)
- ref_combination.insert(idx, attr.attribute_value)
-
- variant["combination"] = ref_combination
-
- if not (set(combination) - set(variant["combination"])):
- # check if the combination is a subset of a variant combination
- # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
- return True
-
- for i, attr in enumerate(self.attributes):
- if i == 0:
- continue
-
- combination_source = []
-
- # loop through previous attributes
- for prev_attr in self.attributes[:i]:
- combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
-
- combination_source.append(context.attribute_values[attr.attribute])
-
- for combination in itertools.product(*combination_source):
- if not find_variant(combination):
- context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
-
- def set_metatags(self, context):
- context.metatags = frappe._dict({})
-
- safe_description = frappe.utils.to_markdown(self.description)
-
- context.metatags.url = frappe.utils.get_url() + '/' + context.route
-
- if context.website_image:
- if context.website_image.startswith('http'):
- url = context.website_image
- else:
- url = frappe.utils.get_url() + context.website_image
- context.metatags.image = url
-
- context.metatags.description = safe_description[:300]
-
- context.metatags.title = self.item_name or self.item_code
-
- context.metatags['og:type'] = 'product'
- context.metatags['og:site_name'] = 'ERPNext'
-
- def set_shopping_cart_data(self, context):
- from erpnext.shopping_cart.product_info import get_product_info_for_website
- context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
-
def add_default_uom_in_conversion_factor_table(self):
if not self.is_new() and self.has_value_changed("stock_uom"):
self.uoms = []
@@ -507,9 +233,29 @@ class Item(WebsiteGenerator):
"conversion_factor": 1
})
- def update_show_in_website(self):
- if self.disabled:
- self.show_in_website = False
+ 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'):
@@ -602,14 +348,6 @@ class Item(WebsiteGenerator):
frappe.throw(_("Barcode {0} is not a valid {1} code").format(
item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode)
- if item_barcode.barcode != item_barcode.name:
- # if barcode is getting updated , the row name has to reset.
- # Delete previous old row doc and re-enter row as if new to reset name in db.
- item_barcode.set("__islocal", True)
- item_barcode_entry_name = item_barcode.name
- item_barcode.name = None
- frappe.delete_doc("Item Barcode", item_barcode_entry_name)
-
def validate_warehouse_for_reorder(self):
'''Validate Reorder level table for duplicate and conditional mandatory'''
warehouse = []
@@ -649,7 +387,6 @@ class Item(WebsiteGenerator):
)
def on_trash(self):
- super(Item, self).on_trash()
frappe.db.sql("""delete from tabBin where item_code=%s""", self.name)
frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name)
for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}):
@@ -660,15 +397,8 @@ class Item(WebsiteGenerator):
frappe.db.set_value("Item", old_name, "item_name", new_name)
if merge:
- # Validate properties before merging
- if not frappe.db.exists("Item", new_name):
- frappe.throw(_("Item {0} does not exist").format(new_name))
-
- field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"]
- new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)]
- if new_properties != [cstr(self.get(fld)) for fld in field_list]:
- frappe.throw(_("To merge, following properties must be same for both items")
- + ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]))
+ self.validate_properties_before_merge(new_name)
+ self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge):
if merge:
@@ -676,9 +406,8 @@ class Item(WebsiteGenerator):
frappe.msgprint(_("It can take upto few hours for accurate stock values to be visible after merging items."),
indicator="orange", title="Note")
- if self.route:
+ if self.published_in_website:
invalidate_cache_for_item(self)
- clear_cache(self.route)
frappe.db.set_value("Item", new_name, "item_code", new_name)
@@ -718,7 +447,41 @@ class Item(WebsiteGenerator):
msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(
frappe.bold(old_name))
- frappe.throw(_(msg), title=_("Merge not allowed"))
+ frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
+
+ def validate_properties_before_merge(self, new_name):
+ # Validate properties before merging
+ if not frappe.db.exists("Item", new_name):
+ frappe.throw(_("Item {0} does not exist").format(new_name))
+
+ field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"]
+ new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)]
+
+ if new_properties != [cstr(self.get(field)) for field in field_list]:
+ msg = _("To merge, following properties must be same for both items")
+ msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
+ 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)
+
+ msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {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)
@@ -740,16 +503,6 @@ class Item(WebsiteGenerator):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- @frappe.whitelist()
- def copy_specification_from_item_group(self):
- self.set("website_specifications", [])
- if self.item_group:
- for label, desc in frappe.db.get_values("Item Website Specification",
- {"parent": self.item_group}, ["label", "description"]):
- row = self.append("website_specifications")
- row.label = label
- row.description = desc
-
def update_bom_item_desc(self):
if self.is_new():
return
@@ -773,25 +526,6 @@ class Item(WebsiteGenerator):
where item_code = %s and docstatus < 2
""", (self.description, self.name))
- def update_template_item(self):
- """Set Show in Website for Template Item if True for its Variant"""
- if not self.variant_of:
- return
-
- if self.show_in_website:
- self.show_variant_in_website = 1
- self.show_in_website = 0
-
- if self.show_variant_in_website:
- # show template
- template_item = frappe.get_doc("Item", self.variant_of)
-
- if not template_item.show_in_website:
- template_item.show_in_website = 1
- template_item.flags.dont_update_variants = True
- template_item.flags.ignore_permissions = True
- template_item.save()
-
def validate_item_defaults(self):
companies = {row.company for row in self.item_defaults}
@@ -1042,47 +776,6 @@ class Item(WebsiteGenerator):
if not enabled:
frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange")
- def create_onboarding_docs(self, args):
- company = frappe.defaults.get_defaults().get('company') or \
- frappe.db.get_single_value('Global Defaults', 'default_company')
-
- for i in range(1, args.get('max_count')):
- item = args.get('item_' + str(i))
- if item:
- default_warehouse = ''
- default_warehouse = frappe.db.get_value('Warehouse', filters={
- 'warehouse_name': _('Finished Goods'),
- 'company': company
- })
-
- try:
- frappe.get_doc({
- 'doctype': self.doctype,
- 'item_code': item,
- 'item_name': item,
- 'description': item,
- 'show_in_website': 1,
- 'is_sales_item': 1,
- 'is_purchase_item': 1,
- 'is_stock_item': 1,
- 'item_group': _('Products'),
- 'stock_uom': _(args.get('item_uom_' + str(i))),
- 'item_defaults': [{
- 'default_warehouse': default_warehouse,
- 'company': company
- }]
- }).insert()
-
- except frappe.NameError:
- pass
- else:
- if args.get('item_price_' + str(i)):
- item_price = flt(args.get('item_price_' + str(i)))
-
- price_list_name = frappe.db.get_value('Price List', {'selling': 1})
- make_item_price(item, price_list_name, item_price)
- price_list_name = frappe.db.get_value('Price List', {'buying': 1})
- make_item_price(item, price_list_name, item_price)
def make_item_price(item, price_list_name, item_price):
frappe.get_doc({
@@ -1197,14 +890,9 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
def invalidate_cache_for_item(doc):
+ """Invalidate Item Group cache and rebuild ItemVariantsCacheManager."""
invalidate_cache_for(doc, doc.item_group)
- website_item_groups = list(set((doc.get("old_website_item_groups") or [])
- + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]))
-
- for item_group in website_item_groups:
- invalidate_cache_for(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)
@@ -1212,12 +900,14 @@ def invalidate_cache_for_item(doc):
def invalidate_item_variants_cache_for_website(doc):
- from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
+ """Rebuild ItemVariantsCacheManager via Item or Website Item."""
+ from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
item_code = None
- if doc.has_variants and doc.show_in_website:
- item_code = doc.name
- elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'show_in_website'):
+ 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:
@@ -1341,10 +1031,6 @@ def update_variants(variants, template, publish_progress=True):
if publish_progress:
frappe.publish_progress(count / total * 100, title=_("Updating Variants..."))
-def on_doctype_update():
- # since route is a Text column, it needs a length for indexing
- frappe.db.add_index("Item", ["route(500)"])
-
@erpnext.allow_regional
def set_item_tax_from_hsn_code(item):
pass
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 0957ce0615..fc45ba99c4 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -536,7 +536,7 @@ class TestItem(ERPNextTestCase):
"check if index is getting created in db"
indices = frappe.db.sql("show index from tabItem", as_dict=1)
- expected_columns = {"item_code", "item_name", "item_group", "route"}
+ expected_columns = {"item_code", "item_name", "item_group"}
for index in indices:
expected_columns.discard(index.get("Column_name"))
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 6cec85288f..91c77d5152 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -40,9 +40,7 @@
"conversion_factor": 10.0
}
],
- "stock_uom": "_Test UOM",
- "show_in_website": 1,
- "website_warehouse": "_Test Warehouse - _TC"
+ "stock_uom": "_Test UOM"
},
{
"description": "_Test Item 2",
@@ -56,8 +54,6 @@
"item_group": "_Test Item Group",
"item_name": "_Test Item 2",
"stock_uom": "_Test UOM",
- "show_in_website": 1,
- "website_warehouse": "_Test Warehouse - _TC",
"gst_hsn_code": "999800",
"opening_stock": 10,
"valuation_rate": 100,
@@ -311,8 +307,7 @@
"warehouse_reorder_level": 20,
"warehouse_reorder_qty": 20
}
- ],
- "show_in_website": 1
+ ]
},
{
"description": "_Test Item 1",
@@ -344,9 +339,7 @@
"warehouse_reorder_qty": 20
}
],
- "stock_uom": "_Test UOM",
- "show_in_website": 1,
- "website_warehouse": "_Test Warehouse Group-C1 - _TC"
+ "stock_uom": "_Test UOM"
},
{
"description": "_Test Item With Item Tax Template",
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
index 488920aadb..5e1f7d5322 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
@@ -7,9 +7,8 @@ frappe.ui.form.on('Item Variant Settings', {
const existing_fields = frm.doc.fields.map(row => row.field_name);
const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name",
- "show_in_website", "show_variant_in_website", "standard_rate", "opening_stock", "image",
- "variant_of", "valuation_rate", "barcodes", "website_image", "thumbnail",
- "website_specifiations", "web_long_description", "has_variants", "attributes"];
+ "published_in_website", "standard_rate", "opening_stock", "image",
+ "variant_of", "valuation_rate", "barcodes", "has_variants", "attributes"];
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
index f63498b9ac..be1517eb58 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
@@ -13,10 +13,9 @@ class ItemVariantSettings(Document):
def set_default_fields(self):
self.fields = []
fields = frappe.get_meta('Item').fields
- exclude_fields = {"naming_series", "item_code", "item_name", "show_in_website",
- "show_variant_in_website", "standard_rate", "opening_stock", "image", "description",
+ exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website",
+ "standard_rate", "opening_stock", "image", "description",
"variant_of", "valuation_rate", "description", "barcodes",
- "website_image", "thumbnail", "website_specifiations", "web_long_description",
"has_variants", "attributes"}
for d in fields:
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 9204842b8f..df8cadd7f8 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -4,10 +4,11 @@
import frappe
-from frappe.utils import flt
+from frappe.utils import add_to_date, flt, now
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
@@ -28,7 +29,8 @@ class TestLandedCostVoucher(ERPNextTestCase):
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
- "warehouse": "Stores - TCP1"
+ "warehouse": "Stores - TCP1",
+ "is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
@@ -41,14 +43,39 @@ class TestLandedCostVoucher(ERPNextTestCase):
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
- "warehouse": "Stores - TCP1"
+ "warehouse": "Stores - TCP1",
+ "is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
-
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0)
+ # assert after submit
+ self.assertPurchaseReceiptLCVGLEntries(pr)
+
+ # Mess up cancelled SLE modified timestamp to check
+ # if they aren't effective in any business logic.
+ frappe.db.set_value("Stock Ledger Entry",
+ {
+ "is_cancelled": 1,
+ "voucher_type": pr.doctype,
+ "voucher_no": pr.name
+ },
+ "is_cancelled", 1,
+ modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True)
+ )
+
+ items, warehouses = pr.get_items_and_warehouses()
+ update_gl_entries_after(pr.posting_date, pr.posting_time,
+ warehouses, items, company=pr.company)
+
+ # reassert after reposting
+ self.assertPurchaseReceiptLCVGLEntries(pr)
+
+
+ def assertPurchaseReceiptLCVGLEntries(self, pr):
+
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -74,8 +101,8 @@ class TestLandedCostVoucher(ERPNextTestCase):
for gle in gl_entries:
if not gle.get('is_cancelled'):
- self.assertEqual(expected_values[gle.account][0], gle.debit)
- self.assertEqual(expected_values[gle.account][1], gle.credit)
+ self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}")
+ self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}")
def test_landed_cost_voucher_against_purchase_invoice(self):
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 830d5469bf..d2d4789765 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -218,8 +218,6 @@
"label": "Conversion Factor"
},
{
- "fetch_from": "item_code.valuation_rate",
- "fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -232,7 +230,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-01 15:10:29.646399",
+ "modified": "2022-01-28 16:03:30.780111",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
@@ -240,5 +238,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index e4091c40dc..07c2f1f0dd 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -8,187 +8,253 @@ import json
import frappe
from frappe.model.document import Document
-from frappe.utils import cstr, flt
+from frappe.utils import flt
-from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.get_item_details import get_item_details, get_price_list_rate
class PackedItem(Document):
pass
-def get_product_bundle_items(item_code):
- return frappe.db.sql("""select t1.item_code, t1.qty, t1.uom, t1.description
- from `tabProduct Bundle Item` t1, `tabProduct Bundle` t2
- where t2.new_item_code=%s and t1.parent = t2.name order by t1.idx""", item_code, as_dict=1)
-
-def get_packing_item_details(item, company):
- return frappe.db.sql("""
- select i.item_name, i.is_stock_item, i.description, i.stock_uom, id.default_warehouse
- from `tabItem` i LEFT JOIN `tabItem Default` id ON id.parent=i.name and id.company=%s
- where i.name = %s""",
- (company, item), as_dict = 1)[0]
-
-def get_bin_qty(item, warehouse):
- det = frappe.db.sql("""select actual_qty, projected_qty from `tabBin`
- where item_code = %s and warehouse = %s""", (item, warehouse), as_dict = 1)
- return det and det[0] or frappe._dict()
-
-def update_packing_list_item(doc, packing_item_code, qty, main_item_row, description):
- if doc.amended_from:
- old_packed_items_map = get_old_packed_item_details(doc.packed_items)
- else:
- old_packed_items_map = False
- item = get_packing_item_details(packing_item_code, doc.company)
-
- # check if exists
- exists = 0
- for d in doc.get("packed_items"):
- if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code:
- if d.parent_detail_docname != main_item_row.name:
- d.parent_detail_docname = main_item_row.name
-
- pi, exists = d, 1
- break
-
- if not exists:
- pi = doc.append('packed_items', {})
-
- pi.parent_item = main_item_row.item_code
- pi.item_code = packing_item_code
- pi.item_name = item.item_name
- pi.parent_detail_docname = main_item_row.name
- pi.uom = item.stock_uom
- pi.qty = flt(qty)
- pi.conversion_factor = main_item_row.conversion_factor
- if description and not pi.description:
- pi.description = description
- if not pi.warehouse and not doc.amended_from:
- pi.warehouse = (main_item_row.warehouse if ((doc.get('is_pos') or item.is_stock_item \
- or not item.default_warehouse) and main_item_row.warehouse) else item.default_warehouse)
- if not pi.batch_no and not doc.amended_from:
- pi.batch_no = cstr(main_item_row.get("batch_no"))
- if not pi.target_warehouse:
- pi.target_warehouse = main_item_row.get("target_warehouse")
- bin = get_bin_qty(packing_item_code, pi.warehouse)
- pi.actual_qty = flt(bin.get("actual_qty"))
- pi.projected_qty = flt(bin.get("projected_qty"))
- if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)):
- pi.batch_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].batch_no
- pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no
- pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse
def make_packing_list(doc):
- """make packing list for Product Bundle item"""
- if doc.get("_action") and doc._action == "update_after_submit": return
-
- parent_items = []
- for d in doc.get("items"):
- if frappe.db.get_value("Product Bundle", {"new_item_code": d.item_code}):
- for i in get_product_bundle_items(d.item_code):
- update_packing_list_item(doc, i.item_code, flt(i.qty)*flt(d.stock_qty), d, i.description)
-
- if [d.item_code, d.name] not in parent_items:
- parent_items.append([d.item_code, d.name])
-
- cleanup_packing_list(doc, parent_items)
-
- if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"):
- update_product_bundle_price(doc, parent_items)
-
-def cleanup_packing_list(doc, parent_items):
- """Remove all those child items which are no longer present in main item table"""
- delete_list = []
- for d in doc.get("packed_items"):
- if [d.parent_item, d.parent_detail_docname] not in parent_items:
- # mark for deletion from doclist
- delete_list.append(d)
-
- if not delete_list:
- return doc
-
- packed_items = doc.get("packed_items")
- doc.set("packed_items", [])
-
- for d in packed_items:
- if d not in delete_list:
- add_item_to_packing_list(doc, d)
-
-def add_item_to_packing_list(doc, packed_item):
- doc.append("packed_items", {
- 'parent_item': packed_item.parent_item,
- 'item_code': packed_item.item_code,
- 'item_name': packed_item.item_name,
- 'uom': packed_item.uom,
- 'qty': packed_item.qty,
- 'rate': packed_item.rate,
- 'conversion_factor': packed_item.conversion_factor,
- 'description': packed_item.description,
- 'warehouse': packed_item.warehouse,
- 'batch_no': packed_item.batch_no,
- 'actual_batch_qty': packed_item.actual_batch_qty,
- 'serial_no': packed_item.serial_no,
- 'target_warehouse': packed_item.target_warehouse,
- 'actual_qty': packed_item.actual_qty,
- 'projected_qty': packed_item.projected_qty,
- 'incoming_rate': packed_item.incoming_rate,
- 'prevdoc_doctype': packed_item.prevdoc_doctype,
- 'parent_detail_docname': packed_item.parent_detail_docname
- })
-
-def update_product_bundle_price(doc, parent_items):
- """Updates the prices of Product Bundles based on the rates of the Items in the bundle."""
-
- if not doc.get('items'):
+ "Make/Update packing list for Product Bundle Item."
+ if doc.get("_action") and doc._action == "update_after_submit":
return
- parent_items_index = 0
- bundle_price = 0
+ parent_items_price, reset = {}, False
+ set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates")
- for bundle_item in doc.get("packed_items"):
- if parent_items[parent_items_index][0] == bundle_item.parent_item:
- bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
- bundle_price += bundle_item.qty * bundle_item_rate
- else:
- update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
+ stale_packed_items_table = get_indexed_packed_items_table(doc)
- bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
- bundle_price = bundle_item.qty * bundle_item_rate
- parent_items_index += 1
+ reset = reset_packing_list(doc)
- # for the last product bundle
- if doc.get("packed_items"):
- update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
+ for item_row in doc.get("items"):
+ if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}):
+ for bundle_item in get_product_bundle_items(item_row.item_code):
+ pi_row = add_packed_item_row(
+ doc=doc, packing_item=bundle_item,
+ main_item_row=item_row, packed_items_table=stale_packed_items_table,
+ reset=reset
+ )
+ item_data = get_packed_item_details(bundle_item.item_code, doc.company)
+ update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
+ update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
+ update_packed_item_price_data(pi_row, item_data, doc)
+ update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
-def update_parent_item_price(doc, parent_item_code, bundle_price):
- parent_item_doc = doc.get('items', {'item_code': parent_item_code})[0]
+ if set_price_from_children: # create/update bundle item wise price dict
+ update_product_bundle_rate(parent_items_price, pi_row)
- current_parent_item_price = parent_item_doc.amount
- if current_parent_item_price != bundle_price:
- parent_item_doc.amount = bundle_price
- update_parent_item_rate(parent_item_doc, bundle_price)
+ if parent_items_price:
+ set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item
-def update_parent_item_rate(parent_item_doc, bundle_price):
- parent_item_doc.rate = bundle_price/parent_item_doc.qty
+def get_indexed_packed_items_table(doc):
+ """
+ Create dict from stale packed items table like:
+ {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}}
-@frappe.whitelist()
-def get_items_from_product_bundle(args):
- args = json.loads(args)
- items = []
- bundled_items = get_product_bundle_items(args["item_code"])
- for item in bundled_items:
- args.update({
- "item_code": item.item_code,
- "qty": flt(args["quantity"]) * flt(item.qty)
- })
- items.append(get_item_details(args))
+ Use: to quickly retrieve/check if row existed in table instead of looping n times
+ """
+ indexed_table = {}
+ for packed_item in doc.get("packed_items"):
+ key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname)
+ indexed_table[key] = packed_item
- return items
+ return indexed_table
+
+def reset_packing_list(doc):
+ "Conditionally reset the table and return if it was reset or not."
+ reset_table = False
+ doc_before_save = doc.get_doc_before_save()
+
+ if doc_before_save:
+ # reset table if:
+ # 1. items were deleted
+ # 2. if bundle item replaced by another item (same no. of items but different items)
+ # we maintain list to track recurring item rows as well
+ items_before_save = [item.item_code for item in doc_before_save.get("items")]
+ items_after_save = [item.item_code for item in doc.get("items")]
+ reset_table = items_before_save != items_after_save
+ else:
+ # reset: if via Update Items OR
+ # if new mapped doc with packed items set (SO -> DN)
+ # (cannot determine action)
+ reset_table = True
+
+ if reset_table:
+ doc.set("packed_items", [])
+ return reset_table
+
+def get_product_bundle_items(item_code):
+ product_bundle = frappe.qb.DocType("Product Bundle")
+ product_bundle_item = frappe.qb.DocType("Product Bundle Item")
+
+ query = (
+ frappe.qb.from_(product_bundle_item)
+ .join(product_bundle).on(product_bundle_item.parent == product_bundle.name)
+ .select(
+ product_bundle_item.item_code,
+ product_bundle_item.qty,
+ product_bundle_item.uom,
+ product_bundle_item.description
+ ).where(
+ product_bundle.new_item_code == item_code
+ ).orderby(
+ product_bundle_item.idx
+ )
+ )
+ return query.run(as_dict=True)
+
+def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset):
+ """Add and return packed item row.
+ doc: Transaction document
+ packing_item (dict): Packed Item details
+ main_item_row (dict): Items table row corresponding to packed item
+ packed_items_table (dict): Packed Items table before save (indexed)
+ reset (bool): State if table is reset or preserved as is
+ """
+ exists, pi_row = False, {}
+
+ # check if row already exists in packed items table
+ key = (main_item_row.item_code, packing_item.item_code, main_item_row.name)
+ if packed_items_table.get(key):
+ pi_row, exists = packed_items_table.get(key), True
+
+ if not exists:
+ pi_row = doc.append('packed_items', {})
+ elif reset: # add row if row exists but table is reset
+ pi_row.idx, pi_row.name = None, None
+ pi_row = doc.append('packed_items', pi_row)
+
+ return pi_row
+
+def get_packed_item_details(item_code, company):
+ item = frappe.qb.DocType("Item")
+ item_default = frappe.qb.DocType("Item Default")
+ query = (
+ frappe.qb.from_(item)
+ .left_join(item_default)
+ .on(
+ (item_default.parent == item.name)
+ & (item_default.company == company)
+ ).select(
+ item.item_name, item.is_stock_item,
+ item.description, item.stock_uom,
+ item.valuation_rate,
+ item_default.default_warehouse
+ ).where(
+ item.name == item_code
+ )
+ )
+ return query.run(as_dict=True)[0]
+
+def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data):
+ pi_row.parent_item = main_item_row.item_code
+ pi_row.parent_detail_docname = main_item_row.name
+ pi_row.item_code = packing_item.item_code
+ pi_row.item_name = item_data.item_name
+ pi_row.uom = item_data.stock_uom
+ pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty)
+ pi_row.conversion_factor = main_item_row.conversion_factor
+
+ if not pi_row.description:
+ pi_row.description = packing_item.get("description")
+
+def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc):
+ # TODO batch_no, actual_batch_qty, incoming_rate
+ if not pi_row.warehouse and not doc.amended_from:
+ fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse)
+ pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse)
+ else item_data.default_warehouse)
+
+ if not pi_row.target_warehouse:
+ pi_row.target_warehouse = main_item_row.get("target_warehouse")
+
+ bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse)
+ pi_row.actual_qty = flt(bin.get("actual_qty"))
+ pi_row.projected_qty = flt(bin.get("projected_qty"))
+
+def update_packed_item_price_data(pi_row, item_data, doc):
+ "Set price as per price list or from the Item master."
+ if pi_row.rate:
+ return
+
+ item_doc = frappe.get_cached_doc("Item", pi_row.item_code)
+ row_data = pi_row.as_dict().copy()
+ row_data.update({
+ "company": doc.get("company"),
+ "price_list": doc.get("selling_price_list"),
+ "currency": doc.get("currency")
+ })
+ rate = get_price_list_rate(row_data, item_doc).get("price_list_rate")
+
+ pi_row.rate = rate or item_data.get("valuation_rate") or 0.0
+
+def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc):
+ "Update packed item row details from cancelled doc into amended doc."
+ prev_doc_packed_items_map = None
+ if doc.amended_from:
+ prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items)
+
+ if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)):
+ prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code))
+ pi_row.batch_no = prev_doc_row[0].batch_no
+ pi_row.serial_no = prev_doc_row[0].serial_no
+ pi_row.warehouse = prev_doc_row[0].warehouse
+
+def get_packed_item_bin_qty(item, warehouse):
+ bin_data = frappe.db.get_values(
+ "Bin",
+ fieldname=["actual_qty", "projected_qty"],
+ filters={"item_code": item, "warehouse": warehouse},
+ as_dict=True
+ )
+
+ return bin_data[0] if bin_data else {}
+
+def get_cancelled_doc_packed_item_details(old_packed_items):
+ prev_doc_packed_items_map = {}
+ for items in old_packed_items:
+ prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict())
+ return prev_doc_packed_items_map
+
+def update_product_bundle_rate(parent_items_price, pi_row):
+ """
+ Update the price dict of Product Bundles based on the rates of the Items in the bundle.
+
+ Stucture:
+ {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0}
+ """
+ key = (pi_row.parent_item, pi_row.parent_detail_docname)
+ rate = parent_items_price.get(key)
+ if not rate:
+ parent_items_price[key] = 0.0
+
+ parent_items_price[key] += flt(pi_row.rate)
+
+def set_product_bundle_rate_amount(doc, parent_items_price):
+ "Set cumulative rate and amount in bundle item."
+ for item in doc.get("items"):
+ bundle_rate = parent_items_price.get((item.item_code, item.name))
+ if bundle_rate and bundle_rate != item.rate:
+ item.rate = bundle_rate
+ item.amount = flt(bundle_rate * item.qty)
def on_doctype_update():
frappe.db.add_index("Packed Item", ["item_code", "warehouse"])
-def get_old_packed_item_details(old_packed_items):
- old_packed_items_map = {}
- for items in old_packed_items:
- old_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict())
- return old_packed_items_map
+
+@frappe.whitelist()
+def get_items_from_product_bundle(row):
+ row, items = json.loads(row), []
+
+ bundled_items = get_product_bundle_items(row["item_code"])
+ for item in bundled_items:
+ row.update({
+ "item_code": item.item_code,
+ "qty": flt(row["quantity"]) * flt(item.qty)
+ })
+ items.append(get_item_details(row))
+
+ return items
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
new file mode 100644
index 0000000000..5cbaa1ea66
--- /dev/null
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.tests.utils import ERPNextTestCase, change_settings
+
+
+class TestPackedItem(ERPNextTestCase):
+ "Test impact on Packed Items table in various scenarios."
+ @classmethod
+ def setUpClass(cls) -> None:
+ make_item("_Test Product Bundle X", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+ make_item("_Test Normal Stock Item", {"is_stock_item": 1})
+
+ make_product_bundle(
+ "_Test Product Bundle X",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"],
+ qty=2
+ )
+
+ def test_adding_bundle_item(self):
+ "Test impact on packed items if bundle item row is added."
+ so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
+ do_not_submit=True)
+
+ self.assertEqual(so.items[0].qty, 1)
+ self.assertEqual(len(so.packed_items), 2)
+ self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1")
+ self.assertEqual(so.packed_items[0].qty, 2)
+
+ def test_updating_bundle_item(self):
+ "Test impact on packed items if bundle item row is updated."
+ so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
+ do_not_submit=True)
+
+ so.items[0].qty = 2 # change qty
+ so.save()
+
+ self.assertEqual(so.packed_items[0].qty, 4)
+ self.assertEqual(so.packed_items[1].qty, 4)
+
+ # change item code to non bundle item
+ so.items[0].item_code = "_Test Normal Stock Item"
+ so.save()
+
+ self.assertEqual(len(so.packed_items), 0)
+
+ def test_recurring_bundle_item(self):
+ "Test impact on packed items if same bundle item is added and removed."
+ so_items = []
+ for qty in [2, 4, 6, 8]:
+ so_items.append({
+ "item_code": "_Test Product Bundle X",
+ "qty": qty,
+ "rate": 400,
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ # create SO with recurring bundle item
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+
+ # check alternate rows for qty
+ self.assertEqual(len(so.packed_items), 8)
+ self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2")
+ self.assertEqual(so.packed_items[1].qty, 4)
+ self.assertEqual(so.packed_items[3].qty, 8)
+ self.assertEqual(so.packed_items[5].qty, 12)
+ self.assertEqual(so.packed_items[7].qty, 16)
+
+ # delete intermediate row (2nd)
+ del so.items[1]
+ so.save()
+
+ # check alternate rows for qty
+ self.assertEqual(len(so.packed_items), 6)
+ self.assertEqual(so.packed_items[1].qty, 4)
+ self.assertEqual(so.packed_items[3].qty, 12)
+ self.assertEqual(so.packed_items[5].qty, 16)
+
+ # delete last row
+ del so.items[2]
+ so.save()
+
+ # check alternate rows for qty
+ self.assertEqual(len(so.packed_items), 4)
+ self.assertEqual(so.packed_items[1].qty, 4)
+ self.assertEqual(so.packed_items[3].qty, 12)
+
+ @change_settings("Selling Settings", {"editable_bundle_item_rates": 1})
+ def test_bundle_item_cumulative_price(self):
+ "Test if Bundle Item rate is cumulative from packed items."
+ so = make_sales_order(item_code = "_Test Product Bundle X", qty=2,
+ do_not_submit=True)
+
+ so.packed_items[0].rate = 150
+ so.packed_items[1].rate = 200
+ so.save()
+
+ self.assertEqual(so.items[0].rate, 350)
+ self.assertEqual(so.items[0].amount, 700)
+
+ def test_newly_mapped_doc_packed_items(self):
+ "Test impact on packed items in newly mapped DN from SO."
+ so_items = []
+ for qty in [2, 4]:
+ so_items.append({
+ "item_code": "_Test Product Bundle X",
+ "qty": qty,
+ "rate": 400,
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ # create SO with recurring bundle item
+ so = make_sales_order(item_list=so_items)
+
+ dn = make_delivery_note(so.name)
+ dn.items[1].qty = 3 # change second row qty for inserting doc
+ dn.save()
+
+ self.assertEqual(len(dn.packed_items), 4)
+ self.assertEqual(dn.packed_items[2].qty, 6)
+ self.assertEqual(dn.packed_items[3].qty, 6)
\ No newline at end of file
diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py
index 74b823ada3..8a3172e9e2 100644
--- a/erpnext/stock/doctype/price_list/price_list.py
+++ b/erpnext/stock/doctype/price_list/price_list.py
@@ -36,14 +36,14 @@ 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 Shopping Cart."
- from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+ "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.get_cached_value("Shopping Cart Settings", None, "price_list")
+ affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list")
if currency_changed and affects_cart:
validate_cart_settings()
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 112ddedac2..b54a90eed3 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -106,6 +106,8 @@
"terms",
"bill_no",
"bill_date",
+ "accounting_details_section",
+ "provisional_expense_account",
"more_info",
"project",
"status",
@@ -1144,16 +1146,30 @@
"label": "Represents Company",
"options": "Company",
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "provisional_expense_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Provisional Expense Account",
+ "options": "Account"
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-28 13:11:10.181328",
+ "modified": "2022-02-01 11:40:52.690984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -1214,6 +1230,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index c97b306c4e..ffdf8c420c 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -8,6 +8,7 @@ from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, getdate, nowdate
+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
@@ -112,6 +113,7 @@ class PurchaseReceipt(BuyingController):
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_cwip_accounts()
+ self.validate_provisional_expense_account()
self.check_on_hold_or_closed_status()
@@ -133,6 +135,15 @@ class PurchaseReceipt(BuyingController):
company = self.company)
break
+ def validate_provisional_expense_account(self):
+ provisional_accounting_for_non_stock_items = \
+ cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
+
+ if provisional_accounting_for_non_stock_items:
+ default_provisional_account = self.get_company_default("default_provisional_account")
+ if not self.provisional_expense_account:
+ self.provisional_expense_account = default_provisional_account
+
def validate_with_previous_doc(self):
super(PurchaseReceipt, self).validate_with_previous_doc({
"Purchase Order": {
@@ -258,13 +269,15 @@ class PurchaseReceipt(BuyingController):
get_purchase_document_details,
)
- 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")
- auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
+ 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'))
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
@@ -273,7 +286,7 @@ class PurchaseReceipt(BuyingController):
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}, "stock_value_difference")
+ "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference")
if not stock_value_diff:
continue
@@ -422,43 +435,58 @@ class PurchaseReceipt(BuyingController):
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)
- elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
- service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
- credit_currency = get_account_currency(service_received_but_not_billed_account)
- debit_currency = get_account_currency(d.expense_account)
- remarks = self.get("remarks") or _("Accounting Entry for Service")
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=service_received_but_not_billed_account,
- cost_center=d.cost_center,
- debit=0.0,
- credit=d.amount,
- remarks=remarks,
- against_account=d.expense_account,
- account_currency=credit_currency,
- project=d.project,
- voucher_detail_no=d.name, item=d)
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=d.expense_account,
- cost_center=d.cost_center,
- debit=d.amount,
- credit=0.0,
- remarks=remarks,
- against_account=service_received_but_not_billed_account,
- account_currency = debit_currency,
- project=d.project,
- voucher_detail_no=d.name,
- item=d)
+ 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:
+ self.add_provisional_gl_entry(d, gl_entries, self.posting_date)
if warehouse_with_no_account:
frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
"\n".join(warehouse_with_no_account))
+ def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0):
+ provisional_expense_account = self.get('provisional_expense_account')
+ credit_currency = get_account_currency(provisional_expense_account)
+ debit_currency = get_account_currency(item.expense_account)
+ expense_account = item.expense_account
+ remarks = self.get("remarks") or _("Accounting Entry for Service")
+ multiplication_factor = 1
+
+ if reverse:
+ multiplication_factor = -1
+ expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account'])
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=provisional_expense_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=multiplication_factor * item.amount,
+ remarks=remarks,
+ against_account=expense_account,
+ account_currency=credit_currency,
+ project=item.project,
+ voucher_detail_no=item.name,
+ item=item,
+ posting_date=posting_date)
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=expense_account,
+ cost_center=item.cost_center,
+ debit=multiplication_factor * item.amount,
+ credit=0.0,
+ remarks=remarks,
+ against_account=provisional_expense_account,
+ account_currency = debit_currency,
+ project=item.project,
+ voucher_detail_no=item.name,
+ item=item,
+ posting_date=posting_date)
+
def make_tax_gl_entries(self, gl_entries):
- expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
+
+ 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')])
# Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {}
@@ -515,7 +543,8 @@ class PurchaseReceipt(BuyingController):
def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None,
- project=None, voucher_detail_no=None, item=None):
+ project=None, voucher_detail_no=None, item=None, posting_date=None):
+
gl_entry = {
"account": account,
"cost_center": cost_center,
@@ -534,6 +563,9 @@ class PurchaseReceipt(BuyingController):
if credit_in_account_currency:
gl_entry.update({"credit_in_account_currency": credit_in_account_currency})
+ if posting_date:
+ gl_entry.update({"posting_date": posting_date})
+
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
def get_asset_gl_entry(self, gl_entries):
@@ -562,6 +594,7 @@ class PurchaseReceipt(BuyingController):
# 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,
@@ -577,6 +610,7 @@ class PurchaseReceipt(BuyingController):
# 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,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 2909a2d2e7..b87d9205e0 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1312,58 +1312,6 @@ class TestPurchaseReceipt(ERPNextTestCase):
self.assertEqual(pr.status, "To Bill")
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
- def test_service_item_purchase_with_perpetual_inventory(self):
- company = '_Test Company with perpetual inventory'
- service_item = '_Test Non Stock Item'
-
- before_test_value = frappe.db.get_value(
- 'Company', company, 'enable_perpetual_inventory_for_non_stock_items'
- )
- frappe.db.set_value(
- 'Company', company,
- 'enable_perpetual_inventory_for_non_stock_items', 1
- )
- srbnb_account = 'Stock Received But Not Billed - TCP1'
- frappe.db.set_value(
- 'Company', company,
- 'service_received_but_not_billed', srbnb_account
- )
-
- pr = make_purchase_receipt(
- company=company, item=service_item,
- warehouse='Finished Goods - TCP1', do_not_save=1
- )
- item_row_with_diff_rate = frappe.copy_doc(pr.items[0])
- item_row_with_diff_rate.rate = 100
- pr.append('items', item_row_with_diff_rate)
-
- pr.save()
- pr.submit()
-
- item_one_gl_entry = frappe.db.get_all("GL Entry", {
- 'voucher_type': pr.doctype,
- 'voucher_no': pr.name,
- 'account': srbnb_account,
- 'voucher_detail_no': pr.items[0].name
- }, pluck="name")
-
- item_two_gl_entry = frappe.db.get_all("GL Entry", {
- 'voucher_type': pr.doctype,
- 'voucher_no': pr.name,
- 'account': srbnb_account,
- 'voucher_detail_no': pr.items[1].name
- }, pluck="name")
-
- # check if the entries are not merged into one
- # seperate entries should be made since voucher_detail_no is different
- self.assertEqual(len(item_one_gl_entry), 1)
- self.assertEqual(len(item_two_gl_entry), 1)
-
- frappe.db.set_value(
- 'Company', company,
- 'enable_perpetual_inventory_for_non_stock_items', before_test_value
- )
-
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
make_purchase_receipt as create_purchase_receipt,
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 30ea1c3cad..e5994b2dd4 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -976,7 +976,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-15 15:46:10.591600",
+ "modified": "2022-02-01 11:32:27.980524",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
@@ -985,5 +985,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
index cd7e63b18b..0ba97d59a1 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "REPOST-ITEM-VAL-.######",
- "creation": "2020-10-22 22:27:07.742161",
+ "creation": "2022-01-11 15:03:38.273179",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -129,7 +129,7 @@
"reqd": 1
},
{
- "default": "0",
+ "default": "1",
"fieldname": "allow_negative_stock",
"fieldtype": "Check",
"label": "Allow Negative Stock"
@@ -177,7 +177,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-11-24 02:18:10.524560",
+ "modified": "2022-01-18 10:57:33.450907",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",
@@ -227,5 +227,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index fb3b355fb7..01c5e3e4e2 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -27,8 +27,7 @@ class RepostItemValuation(Document):
self.item_code = None
self.warehouse = None
- self.allow_negative_stock = self.allow_negative_stock or \
- cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+ self.allow_negative_stock = 1
def set_company(self):
if self.based_on == "Transaction":
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 2947fafe52..ee08e38f33 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -402,10 +402,16 @@ def update_serial_nos(sle, item_det):
def get_auto_serial_nos(serial_no_series, qty):
serial_nos = []
for i in range(cint(qty)):
- serial_nos.append(make_autoname(serial_no_series, "Serial No"))
+ serial_nos.append(get_new_serial_number(serial_no_series))
return "\n".join(serial_nos)
+def get_new_serial_number(series):
+ sr_no = make_autoname(series, "Serial No")
+ if frappe.db.exists("Serial No", sr_no):
+ sr_no = get_new_serial_number(series)
+ return sr_no
+
def auto_make_serial_nos(args):
serial_nos = get_serial_nos(args.get('serial_no'))
created_numbers = []
@@ -459,6 +465,13 @@ def get_serial_nos(serial_no):
return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
if s.strip()]
+def clean_serial_no_string(serial_no: str) -> str:
+ if not serial_no:
+ return ""
+
+ serial_no_list = get_serial_nos(serial_no)
+ return "\n".join(serial_no_list)
+
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
if args.get(field):
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 99000d1201..f8cea71725 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -8,8 +8,10 @@
import frappe
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -21,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase
class TestSerialNo(ERPNextTestCase):
+
+ def tearDown(self):
+ frappe.db.rollback()
+
def test_cannot_create_direct(self):
frappe.delete_doc_if_exists("Serial No", "_TCSER0001")
@@ -176,6 +182,24 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.purchase_document_no, se.name)
+ def test_auto_creation_of_serial_no(self):
+ """
+ Test if auto created Serial No excludes existing serial numbers
+ """
+ item_code = make_item("_Test Auto Serial Item ", {
+ "has_serial_no": 1,
+ "serial_no_series": "XYZ.###"
+ }).item_code
+
+ # Reserve XYZ005
+ pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005")
+ # XYZ005 is already used and will throw an error if used again
+ pr_2 = make_purchase_receipt(item_code=item_code, qty=10)
+
+ self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005")
+ for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no):
+ self.assertNotEqual(serial_no, "XYZ005")
+
def test_serial_no_sanitation(self):
"Test if Serial No input is sanitised before entering the DB."
item_code = "_Test Serialized Item"
@@ -192,7 +216,28 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021")
- frappe.db.rollback()
+ def test_correct_serial_no_incoming_rate(self):
+ """ Check correct consumption rate based on serial no record.
+ """
+ item_code = "_Test Serialized Item"
+ warehouse = "_Test Warehouse - _TC"
+ serial_nos = ["LOWVALUATION", "HIGHVALUATION"]
+
+ in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42,
+ serial_no=serial_nos[0])
+ in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113,
+ serial_no=serial_nos[1])
+
+ out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True)
+
+ # change serial no
+ out.items[0].serial_no = serial_nos[1]
+ out.save()
+ out.submit()
+
+ value_diff = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_no": out.name, "voucher_type": "Delivery Note"},
+ "stock_value_difference"
+ )
+ self.assertEqual(value_diff, -113)
- def tearDown(self):
- frappe.db.rollback()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a61b3199c0..c51c9bc5f4 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1445,14 +1445,15 @@ class StockEntry(StockController):
qty = req_qty_each * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code):
+ precision = frappe.get_precision("Stock Entry Detail", "qty")
for d in backflushed_materials.get(item.item_code):
- if d.get(item.warehouse):
+ if d.get(item.warehouse) > 0:
if (qty > req_qty):
- qty = (qty/trans_qty) * flt(self.fg_completed_qty)
+ qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision))
+ / (flt(trans_qty, precision) - flt(produced_qty, precision))
+ ) * flt(self.fg_completed_qty)
- if consumed_qty and frappe.db.get_single_value("Manufacturing Settings",
- "material_consumption"):
- qty -= consumed_qty
+ d[item.warehouse] -= qty
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
qty = frappe.utils.ceil(qty)
@@ -1672,6 +1673,8 @@ class StockEntry(StockController):
for d in self.get("items"):
item_code = d.get('original_item') or d.get('item_code')
reserve_warehouse = item_wh.get(item_code)
+ if not (reserve_warehouse and item_code):
+ continue
stock_bin = get_bin(item_code, reserve_warehouse)
stock_bin.update_reserved_qty_for_sub_contracting()
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 8f5d442d19..306f2c3e69 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -851,6 +851,34 @@ class TestStockEntry(ERPNextTestCase):
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
+ def test_zero_incoming_rate(self):
+ """ Make sure incoming rate of 0 is allowed while consuming.
+
+ qty | rate | valuation rate
+ 1 | 100 | 100
+ 1 | 0 | 50
+ -1 | 100 | 0
+ -1 | 0 <--- assert this
+ """
+ item_code = "_TestZeroVal"
+ warehouse = "_Test Warehouse - _TC"
+ create_item('_TestZeroVal')
+ _receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100)
+ receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True)
+ receipt2.items[0].allow_zero_valuation_rate = 1
+ receipt2.save()
+ receipt2.submit()
+
+ issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
+
+ value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
+ self.assertEqual(value_diff, -100)
+
+ issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
+ value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
+ self.assertEqual(value_diff, 0)
+
+
def test_gle_for_opening_stock_entry(self):
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index cafbd7581c..a1030d5496 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -5,7 +5,10 @@ import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today
-from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.delivery_note.test_delivery_note import (
+ create_delivery_note,
+ create_return_delivery_note,
+)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher,
@@ -232,8 +235,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150
- return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
- company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+ return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2)
# check incoming rate for Return entry
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index c4ddc9e2d6..428370cc75 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,7 +6,7 @@
import frappe
-from frappe.utils import add_days, flt, nowdate, nowtime, random_string
+from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.doctype.item.test_item import create_item
@@ -439,8 +439,8 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertRaises(frappe.ValidationError, sr.submit)
def test_serial_no_cancellation(self):
-
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
if not item.has_serial_no:
item.has_serial_no = 1
@@ -466,6 +466,31 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertEqual(len(active_sr_no), 10)
+ def test_serial_no_creation_and_inactivation(self):
+ item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1)
+ if not item.has_serial_no:
+ item.has_serial_no = 1
+ item.save()
+
+ item_code = item.name
+ warehouse = "_Test Warehouse - _TC"
+
+ sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse,
+ serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100)
+ sr.save()
+ self.assertEqual(cstr(sr.items[0].current_serial_no), "")
+ sr.submit()
+
+ active_sr_no = frappe.get_all("Serial No",
+ filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+ self.assertEqual(len(active_sr_no), 1)
+
+ sr.cancel()
+ active_sr_no = frappe.get_all("Serial No",
+ filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+ self.assertEqual(len(active_sr_no), 0)
+
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
if not batch_item_doc.has_batch_no:
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 33d9a6ce41..438ec16096 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -5,35 +5,41 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "defaults_tab",
"item_defaults_section",
"item_naming_by",
"item_group",
"stock_uom",
- "default_warehouse",
"column_break_4",
- "valuation_method",
+ "default_warehouse",
"sample_retention_warehouse",
- "use_naming_series",
- "naming_series_prefix",
+ "valuation_method",
+ "price_list_defaults_section",
+ "auto_insert_price_list_rate_if_missing",
+ "column_break_12",
+ "update_existing_price_list_rate",
+ "stock_validations_tab",
"section_break_9",
"over_delivery_receipt_allowance",
- "role_allowed_to_over_deliver_receive",
"mr_qty_allowance",
- "column_break_12",
- "auto_insert_price_list_rate_if_missing",
- "update_existing_price_list_rate",
+ "column_break_121",
+ "role_allowed_to_over_deliver_receive",
"allow_negative_stock",
"show_barcode_field",
"clean_description_html",
"quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
- "column_break_21",
+ "column_break_23",
"action_if_quality_inspection_is_rejected",
+ "serial_and_batch_item_settings_tab",
"section_break_7",
"automatically_set_serial_nos_based_on_fifo",
"set_qty_in_transactions_based_on_serial_no_input",
"column_break_10",
"disable_serial_no_and_batch_selector",
+ "use_naming_series",
+ "naming_series_prefix",
+ "stock_planning_tab",
"auto_material_request",
"auto_indent",
"column_break_27",
@@ -42,6 +48,7 @@
"allow_from_dn",
"column_break_31",
"allow_from_pr",
+ "stock_closing_tab",
"control_historical_stock_transactions_section",
"stock_frozen_upto",
"stock_frozen_upto_days",
@@ -122,7 +129,7 @@
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
- "label": "Serialised and Batch Setting"
+ "label": "Serial & Batch Item Settings"
},
{
"default": "0",
@@ -275,10 +282,6 @@
"fieldtype": "Section Break",
"label": "Quality Inspection Settings"
},
- {
- "fieldname": "column_break_21",
- "fieldtype": "Column Break"
- },
{
"default": "Stop",
"fieldname": "action_if_quality_inspection_is_rejected",
@@ -298,6 +301,44 @@
"fieldname": "update_existing_price_list_rate",
"fieldtype": "Check",
"label": "Update Existing Price List Rate"
+ },
+ {
+ "fieldname": "defaults_tab",
+ "fieldtype": "Tab Break",
+ "label": "Defaults"
+ },
+ {
+ "fieldname": "stock_validations_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Validations"
+ },
+ {
+ "fieldname": "stock_planning_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Planning"
+ },
+ {
+ "fieldname": "stock_closing_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Closing"
+ },
+ {
+ "fieldname": "serial_and_batch_item_settings_tab",
+ "fieldtype": "Tab Break",
+ "label": "Serial & Batch Item"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "price_list_defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Price List Defaults"
+ },
+ {
+ "fieldname": "column_break_121",
+ "fieldtype": "Column Break"
}
],
"icon": "icon-cog",
@@ -305,7 +346,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-11-06 19:40:02.183592",
+ "modified": "2022-02-04 15:33:43.692736",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
@@ -324,5 +365,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json
deleted file mode 100644
index 5ee316786c..0000000000
--- a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "add_more_button": 1,
- "app": "ERPNext",
- "creation": "2019-11-15 14:41:12.007359",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:54:09.602885",
- "modified_by": "Administrator",
- "name": "Add A Few Products You Buy Or Sell",
- "owner": "Administrator",
- "ref_doctype": "Item",
- "slide_desc": "",
- "slide_fields": [
- {
- "align": "",
- "fieldname": "item",
- "fieldtype": "Data",
- "label": "Item",
- "placeholder": "Product Name",
- "reqd": 1
- },
- {
- "align": "",
- "fieldname": "item_price",
- "fieldtype": "Currency",
- "label": "Item Price",
- "reqd": 1
- },
- {
- "align": "",
- "fieldtype": "Column Break",
- "reqd": 0
- },
- {
- "align": "",
- "fieldname": "uom",
- "fieldtype": "Link",
- "label": "UOM",
- "options": "UOM",
- "reqd": 1
- }
- ],
- "slide_order": 30,
- "slide_title": "Add A Few Products You Buy Or Sell",
- "slide_type": "Create"
-}
\ No newline at end of file
diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py
index 44e13869dd..87097c72fa 100644
--- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py
+++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py
@@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters):
return frappe.db.sql("""select item_code, batch_no, warehouse,
posting_date, actual_qty
from `tabStock Ledger Entry`
- where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
+ where is_cancelled = 0
+ and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision):
diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py
index 5f6184d6f3..058af77aa2 100644
--- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py
+++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py
@@ -91,7 +91,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis
voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
svd_list = frappe.get_list(
'Stock Ledger Entry', fields=['item_code','stock_value_difference'],
- filters=[('voucher_no', 'in', voucher_nos)]
+ filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)]
)
assign_item_groups_to_svd_list(svd_list)
return svd_list
diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
index 3f490653e1..cfa1e474c7 100644
--- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
+++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
@@ -76,6 +76,7 @@ def get_consumed_items(condition):
on sle.voucher_no = se.name
where
actual_qty < 0
+ and is_cancelled = 0
and voucher_type not in ('Delivery Note', 'Sales Invoice')
%s
group by item_code""" % condition, as_dict=1)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index fe2417bba7..ef7c2cc7d9 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -86,10 +86,10 @@ frappe.query_reports["Stock Ledger"] = {
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (column.fieldname == "out_qty" && data.out_qty < 0) {
+ if (column.fieldname == "out_qty" && data && data.out_qty < 0) {
value = "
" + value + " ";
}
- else if (column.fieldname == "in_qty" && data.in_qty > 0) {
+ else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
value = "
" + value + " ";
}
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index c60a6ca56e..81fa0458f2 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -104,6 +104,7 @@ def get_columns():
{"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
+ {"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 1dcf863a9d..525af40b41 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -1,6 +1,8 @@
import unittest
from typing import List, Tuple
+import frappe
+
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
@@ -10,8 +12,12 @@ DEFAULT_FILTERS = {
}
+batch = frappe.db.get_value("Batch", fieldname=["name"], as_dict=True, order_by="creation desc")
+
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}),
+ ("Stock Ledger", {"batch_no": batch}),
+ ("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}),
("Stock Balance", {"_optional": True}),
("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}),
@@ -40,6 +46,13 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Item Variant Details", {"item": "_Test Variant Item",}),
("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}),
+ ("Incorrect Stock Value Report", {"company": "_Test Company with perpetual inventory"}),
+ ("Incorrect Serial No Valuation", {}),
+ ("Incorrect Balance Qty After Transaction", {}),
+ ("Supplier-Wise Sales Analytics", {}),
+ ("Item Prices", {"items": "Enabled Items only"}),
+ ("Delayed Item Report", {"based_on": "Sales Invoice"}),
+ ("Delayed Item Report", {"based_on": "Delivery Note"}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check",
{
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 107bb23222..0a7ab4009c 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -105,6 +105,7 @@ def get_args_for_future_sle(row):
def validate_serial_no(sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
for sn in get_serial_nos(sle.serial_no):
args = copy.deepcopy(sle)
args.serial_no = sn
@@ -423,6 +424,8 @@ class update_entries_after(object):
return sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
@@ -437,7 +440,7 @@ class update_entries_after(object):
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
- if sle.serial_no:
+ if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
@@ -449,8 +452,9 @@ class update_entries_after(object):
# assert
self.wh_data.valuation_rate = sle.valuation_rate
self.wh_data.qty_after_transaction = sle.qty_after_transaction
- self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+ if self.valuation_method != "Moving Average":
+ self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
else:
if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle)
@@ -602,9 +606,9 @@ class update_entries_after(object):
incoming_rate = self.wh_data.valuation_rate
stock_value_change = 0
- if incoming_rate:
+ if actual_qty > 0:
stock_value_change = actual_qty * incoming_rate
- elif actual_qty < 0:
+ else:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
if not sle.is_cancelled:
@@ -646,6 +650,7 @@ class update_entries_after(object):
where
company = %s
and actual_qty > 0
+ and is_cancelled = 0
and (serial_no = %s
or serial_no like %s
or serial_no like %s
@@ -901,6 +906,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
item_code = %s
AND warehouse = %s
AND valuation_rate >= 0
+ AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
@@ -911,6 +917,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
where
item_code = %s
AND valuation_rate > 0
+ AND is_cancelled = 0
AND NOT(voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 3c70b41eda..7c63c17ad0 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -103,7 +103,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
serial_nos = get_serial_nos_data_after_transactions(args)
return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
- if last_entry else (0.0, 0.0, 0.0))
+ if last_entry else (0.0, 0.0, None))
else:
return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
else:
@@ -176,13 +176,7 @@ def get_latest_stock_balance():
def get_bin(item_code, warehouse):
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
if not bin:
- bin_obj = frappe.get_doc({
- "doctype": "Bin",
- "item_code": item_code,
- "warehouse": warehouse,
- })
- bin_obj.flags.ignore_permissions = 1
- bin_obj.insert()
+ bin_obj = _create_bin(item_code, warehouse)
else:
bin_obj = frappe.get_doc('Bin', bin, for_update=True)
bin_obj.flags.ignore_permissions = True
@@ -192,16 +186,24 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record:
- bin_obj = frappe.get_doc({
- "doctype": "Bin",
- "item_code": item_code,
- "warehouse": warehouse,
- })
+ bin_obj = _create_bin(item_code, warehouse)
+ bin_record = bin_obj.name
+ return bin_record
+
+def _create_bin(item_code, warehouse):
+ """Create a bin and take care of concurrent inserts."""
+
+ bin_creation_savepoint = "create_bin"
+ try:
+ frappe.db.savepoint(bin_creation_savepoint)
+ bin_obj = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
bin_obj.flags.ignore_permissions = 1
bin_obj.insert()
- bin_record = bin_obj.name
+ except frappe.UniqueValidationError:
+ frappe.db.rollback(save_point=bin_creation_savepoint) # preserve transaction in postgres
+ bin_obj = frappe.get_last_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
- return bin_record
+ return bin_obj
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
"""WARNING: This function is deprecated. Inline this function instead of using it."""
@@ -419,6 +421,19 @@ def is_reposting_item_valuation_in_progress():
if reposting_in_progress:
frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
+
+def calculate_mapped_packed_items_return(return_doc):
+ parent_items = set([item.parent_item for item in return_doc.packed_items])
+ against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against)
+
+ for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items):
+ if original_bundle.item_code in parent_items:
+ for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items):
+ if returned_packed_item.parent_item == original_bundle.item_code:
+ returned_packed_item.parent_detail_docname = returned_bundle.name
+ returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty
+
+
def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
"""Check if there are pending reposting job till the specified posting date."""
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 4df27f5dbf..ed33067e73 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -1,10 +1,11 @@
{
"charts": [
{
- "chart_name": "Warehouse wise Stock Value"
+ "chart_name": "Warehouse wise Stock Value",
+ "label": "Warehouse wise Stock Value"
}
],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Stock\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Quick Access\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Material Request\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Receipt\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Delivery Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Masters & Reports\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items and Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Stock Transactions\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Stock Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Serial No and Batch\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Incorrect Data Report\", \"col\": 4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Stock\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Warehouse wise Stock Value\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Quick Access \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Receipt\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Masters & Reports \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Transactions\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Serial No and Batch\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:43:10.096528",
"docstatus": 0,
"doctype": "Workspace",
@@ -706,7 +707,7 @@
"type": "Link"
}
],
- "modified": "2021-11-23 04:34:00.420870",
+ "modified": "2022-01-13 17:47:38.339931",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
@@ -715,7 +716,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 24,
+ "sequence_id": 24.0,
"shortcuts": [
{
"color": "Green",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index bfbffe22ad..4dbb0e7e86 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -111,6 +111,7 @@ frappe.ui.form.on('Service Level Agreement', {
filters: [
['DocType', 'issingle', '=', 0],
['DocType', 'istable', '=', 0],
+ ['DocType', 'is_submittable', '=', 0],
['DocType', 'name', 'not in', invalid_doctypes],
['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
]
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index ea617fd1eb..526b6aa249 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -29,6 +29,7 @@ from erpnext.support.doctype.issue.issue import get_holidays
class ServiceLevelAgreement(Document):
def validate(self):
+ self.validate_selected_doctype()
self.validate_doc()
self.validate_status_field()
self.check_priorities()
@@ -106,6 +107,23 @@ class ServiceLevelAgreement(Document):
frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format(
frappe.bold(self.entity_type), frappe.bold(self.entity)))
+ def validate_selected_doctype(self):
+ invalid_doctypes = list(frappe.model.core_doctypes_list)
+ invalid_doctypes.extend(['Cost Center', 'Company'])
+ valid_document_types = frappe.get_all('DocType', {
+ 'issingle': 0,
+ 'istable': 0,
+ 'is_submittable': 0,
+ 'name': ['not in', invalid_doctypes],
+ 'module': ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
+ }, pluck="name")
+
+ if self.document_type not in valid_document_types:
+ frappe.throw(
+ msg=_("Please select valid document type."),
+ title=_("Invalid Document Type")
+ )
+
def validate_status_field(self):
meta = frappe.get_meta(self.document_type)
if not meta.get_field("status"):
@@ -247,9 +265,15 @@ def get_active_service_level_agreement_for(doc):
]
customer = doc.get('customer')
- or_filters.append(
- ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)]
- )
+ if customer:
+ or_filters.extend([
+ ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)],
+ ["Service Level Agreement", "entity_type", "is", "not set"]
+ ])
+ else:
+ or_filters.append(
+ ["Service Level Agreement", "entity_type", "is", "not set"]
+ )
default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter,
@@ -361,11 +385,18 @@ def apply(doc, method=None):
sla = get_active_service_level_agreement_for(doc)
if not sla:
+ remove_sla_if_applied(doc)
return
process_sla(doc, sla)
+def remove_sla_if_applied(doc):
+ doc.service_level_agreement = None
+ doc.response_by = None
+ doc.resolution_by = None
+
+
def process_sla(doc, sla):
if not doc.creation:
@@ -670,7 +701,7 @@ def on_communication_update(doc, status):
update_response_and_resolution_metrics(parent, for_resolution)
update_agreement_status(parent, for_resolution)
- parent.save()
+ parent.save(ignore_permissions=True)
def reset_expected_response_and_resolution(doc):
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
deleted file mode 100644
index 22e2c374e1..0000000000
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- 'fieldname': 'service_level_agreement',
- 'transactions': [
- {
- 'label': _('Issue'),
- 'items': ['Issue']
- }
- ]
- }
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index b07c862c7b..a34124fba2 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -244,6 +244,13 @@ class TestServiceLevelAgreement(unittest.TestCase):
applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement')
self.assertEqual(applied_sla, lead_sla.name)
+ # check if SLA is removed if condition fails
+ lead.reload()
+ lead.source = None
+ lead.save()
+ applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement')
+ self.assertFalse(applied_sla)
+
def tearDown(self):
for d in frappe.get_all("Service Level Agreement"):
frappe.delete_doc("Service Level Agreement", d.name, force=1)
diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json
index d68c7c70cf..8ca3a676c9 100644
--- a/erpnext/support/workspace/support/support.json
+++ b/erpnext/support/workspace/support/support.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Issue\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Maintenance Visit\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Service Level Agreement\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Issues\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Service Level Agreement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Warranty\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Issue\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Maintenance Visit\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Service Level Agreement\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Issues\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Service Level Agreement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Warranty\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:48:23.224699",
"docstatus": 0,
"doctype": "Workspace",
@@ -169,7 +169,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:02.699924",
+ "modified": "2022-01-13 17:48:27.247406",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
@@ -178,7 +178,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 25,
+ "sequence_id": 25.0,
"shortcuts": [
{
"color": "Yellow",
diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html
index 17f6880293..4070d40d47 100644
--- a/erpnext/templates/generators/item/item.html
+++ b/erpnext/templates/generators/item/item.html
@@ -1,4 +1,5 @@
{% extends "templates/web.html" %}
+{% from "erpnext/templates/includes/macros.html" import recommended_item_row %}
{% block title %} {{ title }} {% endblock %}
@@ -9,25 +10,70 @@
{% 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" %}
-
- {% include "templates/generators/item/item_specifications.html" %}
-
- {{ doc.website_content or '' }}
+
+
+
+ {% 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") }}
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index 167c848eff..8000a2446b 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -5,54 +5,115 @@
+
{% if cart_settings.show_price and product_info.price %}
-
- {{ product_info.price.formatted_price_sales_uom }}
- ({{ product_info.price.formatted_price }} / {{ product_info.uom }})
-
+ {% set price_info = product_info.price %}
+
+
+
+ {{ price_info.formatted_price_sales_uom }}
+
+
+ {% 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.in_stock == 0 %}
-
- {{ _('Not in stock') }}
-
+
+ {% 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[0][0] }})
- {% endif %}
-
+
+ {{ _('In stock') }}
+ {% if product_info.show_stock_qty and product_info.stock_qty %}
+ ({{ product_info.stock_qty[0][0] }})
+ {% endif %}
+
{% endif %}
{% endif %}
-
- {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
-
- {{ _("View in Cart") }}
-
-
-
-
-
+
+
+ {% if doc.offers %}
+
+
+
+
+
+ Available Offers
+
+
+ {% for offer in doc.offers %}
+
+
+
+
+
+
+
-
- {{ _("Add to Cart") }}
-
- {% endif %}
- {% if cart_settings.show_contact_us_button %}
- {% include "templates/generators/item/item_inquiry.html" %}
- {% endif %}
+
+
+ {{ _(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 %}
+
@@ -60,10 +121,11 @@
{% endif %}
diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html
index b61ac73072..e97a275fbd 100644
--- a/erpnext/templates/generators/item/item_configure.html
+++ b/erpnext/templates/generators/item/item_configure.html
@@ -3,11 +3,11 @@
{% if cart_settings.enable_variants | int %}
-
- {{ _('Configure') }}
+ {{ _('Select Variant') }}
{% endif %}
{% if cart_settings.show_contact_us_button %}
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index 8eadb84289..231ae0587e 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -29,7 +29,7 @@ class ItemConfigure {
});
this.dialog = new frappe.ui.Dialog({
- title: __('Configure {0}', [this.item_name]),
+ title: __('Select Variant for {0}', [this.item_name]),
fields,
on_hide: () => {
set_continue_configuration();
@@ -201,7 +201,7 @@ class ItemConfigure {
${frappe.utils.icon('assets', 'md')}
- ${__("Add to Cart")}s
+ ${__("Add to Cart")}
` : '';
@@ -214,7 +214,7 @@ class ItemConfigure {
? `
${one_item}
- ${product_info && product_info.price
+ ${product_info && product_info.price && !$.isEmptyObject(product_info.price)
? '(' + product_info.price.formatted_price_sales_uom + ')'
: ''
}
@@ -247,7 +247,7 @@ class ItemConfigure {
const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
return `${attribute}: ${this.range_values[attribute]}`;
}).join('\n');
- erpnext.shopping_cart.update_cart({
+ erpnext.e_commerce.shopping_cart.update_cart({
item_code,
additional_notes,
qty: 1
@@ -280,14 +280,14 @@ class ItemConfigure {
}
get_next_attribute_and_values(selected_attributes) {
- return this.call('erpnext.portal.product_configurator.utils.get_next_attribute_and_values', {
+ 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.portal.product_configurator.utils.get_attributes_and_values', {
+ return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', {
item_code: this.item_code
});
}
@@ -311,9 +311,9 @@ function set_continue_configuration() {
const { itemCode } = $btn_configure.data();
if (localStorage.getItem(`configure:${itemCode}`)) {
- $btn_configure.text(__('Continue Configuration'));
+ $btn_configure.text(__('Continue Selection'));
} else {
- $btn_configure.text(__('Configure'));
+ $btn_configure.text(__('Select Variant'));
}
}
diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html
index 3b77585827..028936bf5f 100644
--- a/erpnext/templates/generators/item/item_details.html
+++ b/erpnext/templates/generators/item/item_details.html
@@ -1,27 +1,63 @@
-
-
-
- {{ item_name }}
-
-
- {{ _("Item Code") }}:
- {{ doc.name }}
-
-{% 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 %}
- {{ _("No description given") }}
-{% endif %}
-
+{% 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
index 39a30d0d7c..930bb7a67d 100644
--- a/erpnext/templates/generators/item/item_image.html
+++ b/erpnext/templates/generators/item/item_image.html
@@ -1,29 +1,30 @@
-
+{% set column_size = 5 if slides else 4 %}
+
{% if slides %}
-
- {% for item in slides %}
-
- {% endfor %}
-
- {{ product_image(slides[0].image, 'product-image') }}
-
-
+ $('.item-slideshow-image').removeClass('active');
+ $img.addClass('active');
+ });
+ })
+
{% else %}
- {{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
+ {{ product_image(doc.website_image or doc.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
index 83653b6821..af636f1582 100644
--- a/erpnext/templates/generators/item/item_inquiry.html
+++ b/erpnext/templates/generators/item/item_inquiry.html
@@ -1,9 +1,9 @@
{% 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') }}
-
+ {% if cart_settings.show_contact_us_button | int %}
+
+ {{ _('Contact Us') }}
+
{% endif %}
diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html
index d4dfa8e591..0814d81c8a 100644
--- a/erpnext/templates/generators/item/item_specifications.html
+++ b/erpnext/templates/generators/item/item_specifications.html
@@ -1,14 +1,20 @@
-{% if doc.website_specifications -%}
-
-
-
- {% for d in doc.website_specifications -%}
+
+{% if website_specifications %}
+
+
+ {% if not show_tabs %}
+
+ Product Details
+
+ {% endif %}
+
+ {% for d in website_specifications -%}
- {{ d.label }}
- {{ d.description }}
+ {{ d.label }}
+ {{ d.description }}
{%- endfor %}
-{%- endif %}
+{% endif %}
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index b5f18ba66d..e099cdde6a 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -1,17 +1,25 @@
+{% 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 %}
+ {% if slideshow %}
{{ web_block(
"Hero Slider",
values=slideshow,
@@ -20,91 +28,28 @@
add_bottom_padding=0,
) }}
{% endif %}
-
{{ title }}
- {% if description %}
+
+ {% if description %}
{{ description or ""}}
{% endif %}
-
-
- {% if items %}
- {% for item in items %}
- {% include "erpnext/www/all-products/item_row.html" %}
- {% endfor %}
- {% else %}
- {% include "erpnext/www/all-products/not_found.html" %}
- {% endif %}
-
+
+
+
- {% for field_filter in field_filters %}
- {%- set item_field = field_filter[0] %}
- {%- set values = field_filter[1] %}
-
-
{{ item_field.label }}
+
+ {{ field_filter_section(field_filters) }}
- {% if values | len > 20 %}
-
-
- {% endif %}
+
+ {{ attribute_filter_section(attribute_filters) }}
- {% if values %}
-
- {% for value in values %}
-
-
-
- {{ value }}
-
-
- {% endfor %}
-
- {% else %}
-
{{ _('No values') }}
- {% endif %}
-
- {% endfor %}
-
- {% for attribute in attribute_filters %}
-
-
{{ attribute.name}}
- {% if values | len > 20 %}
-
-
- {% endif %}
-
- {% if attribute.item_attribute_values %}
-
- {% for attr_value in attribute.item_attribute_values %}
-
-
-
- {{ attr_value.attribute_value }}
-
-
- {% endfor %}
-
- {% else %}
-
{{ _('No values') }}
- {% endif %}
-
- {% endfor %}
-
-
-
-
- {% if frappe.form_dict.start|int > 0 %}
-
- {{ _("Prev") }}
-
- {% endif %}
- {% if items|length >= page_length %}
-
- {{ _("Next") }}
-
- {% endif %}
-
-
-->
\ No newline at end of file
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index be0d47f371..4741307737 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -7,9 +7,15 @@
{% endmacro %}
-{% macro product_image(website_image, css_class="product-image", alt="") %}
-
-
+{% macro product_image(website_image, css_class="product-image", alt="", no_border=False) %}
+
+ {% if website_image %}
+
+ {% else %}
+
+ {{ frappe.utils.get_abbr(alt) or "NA" }}
+
+ {% endif %}
{% endmacro %}
@@ -59,65 +65,335 @@
{% endmacro %}
-{%- macro item_card(title, image, url, description, rate, category, is_featured=False, is_full_width=False, align="Left") -%}
+{%- macro item_card(item, is_featured=False, is_full_width=False, align="Left") -%}
{%- set align_items_class = resolve_class({
'align-items-end': align == 'Right',
'align-items-center': align == 'Center',
'align-items-start': align == 'Left',
}) -%}
{%- set col_size = 3 if is_full_width else 4 -%}
+{%- set title = item.web_item_name or item.item_name or item.item_code -%}
+{%- set title = title[:50] + "..." if title|len > 50 else title -%}
+{%- set image = item.website_image or item.image -%}
+{%- set description = item.website_description or item.description-%}
+
{% if is_featured %}
-
+
{% if image %}
-
+
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ {{ item_card_body(title, description, item, is_featured, align) }}
{% else %}
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ {{ item_card_body(title, description, item, is_featured, align) }}
{% endif %}
{% else %}
-
+
{% if image %}
-
-
-
+
{% else %}
-
- {{ frappe.utils.get_abbr(title) }}
-
+
+
+ {{ frappe.utils.get_abbr(title) }}
+
+
{% endif %}
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ {{ item_card_body(title, description, item, is_featured, align) }}
{% endif %}
{%- endmacro -%}
-{%- macro item_card_body(title, description, url, rate, category, is_featured, align) -%}
+{%- macro item_card_body(title, description, item, is_featured, align) -%}
{%- set align_class = resolve_class({
'text-right': align == 'Right',
'text-center': align == 'Center' and not is_featured,
'text-left': align == 'Left' or is_featured,
}) -%}
-
-
{{ title or '' }}
+
+
{% if is_featured %}
-
{{ rate or '' }}
-
{{ description or '' }}
+
+ {{ description or '' }}
+
{% else %}
-
{{ category or '' }}
-
{{ rate or '' }}
+
{{ item.item_group or '' }}
{% endif %}
-
+{%- endmacro -%}
+
+
+{%- macro wishlist_card(item, settings) %}
+{%- set title = item.web_item_name or ''-%}
+{%- set title = title[:90] + "..." if title|len > 90 else title -%}
+
+
+
+
+ {{ wishlist_card_body(item, title, settings) }}
+
+
+{%- endmacro -%}
+
+{%- macro wishlist_card_body(item, title, settings) %}
+
+
+
{{ title or ''}}
+
{{ item.item_group or '' }}
+
+
+ {{ item.get("formatted_price") or '' }}
+
+ {% if item.get("formatted_mrp") %}
+
+ {{ item.formatted_mrp }}
+
+
+ {{ item.discount }} OFF
+
+ {% endif %}
+
+
+ {% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %}
+
+
+
+
+
+
+
+ {{ _("Move to Cart") }}
+
+ {% else %}
+
+ {{ _("Out of stock") }}
+
+ {% endif %}
+
+{%- endmacro -%}
+
+{%- macro ratings_with_title(avg_rating, title, size, rating_header_class, for_summary=False) -%}
+
+
+
+ {% for i in range(1,6) %}
+ {% set fill_class = 'star-click' if i <= avg_rating else '' %}
+
+
+
+ {% endfor %}
+
+
+{%- endmacro -%}
+
+{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=False, total_reviews=None)-%}
+
+
+
+ {{ average_rating or 0 }}
+
+
+ {{ frappe.utils.cstr(total_reviews or 0) + " " + _("ratings") }}
+
+
+
+ {% if reviews %}
+ {% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") if not for_summary else ''%}
+ {{ ratings_with_title(average_whole_rating, rating_title, "md", "rating-summary-title", for_summary) }}
+ {% endif %}
+
+
{{ frappe.utils.cstr(average_rating or 0) + " " + _("out of 5") }}
+
+
+
+
+ {% for percent in reviews_per_rating %}
+
+ {{ loop.index }} star
+
+
+ {% endfor %}
+
+
+{%- endmacro -%}
+
+{%- macro user_review(reviews)-%}
+
+
+ {% for review in reviews %}
+
+ {{ ratings_with_title(review.rating, _(review.review_title), "sm", "user-review-title") }}
+
+
+
+ {{ _(review.comment) }}
+
+
+
+
+ {{ _(review.customer) }}
+
+ {{ review.published_on }}
+
+
+ {% endfor %}
+
+{%- endmacro -%}
+
+{%- macro field_filter_section(filters)-%}
+{% for field_filter in filters %}
+ {%- set item_field = field_filter[0] %}
+ {%- set values = field_filter[1] %}
+
+
{{ item_field.label }}
+
+ {% if values | len > 20 %}
+
+
+ {% endif %}
+
+ {% if values %}
+
+ {% for value in values %}
+
+
+
+ {{ value }}
+
+
+ {% endfor %}
+
+ {% else %}
+
{{ _('No values') }}
+ {% endif %}
+
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro attribute_filter_section(filters)-%}
+{% for attribute in filters %}
+
+
{{ attribute.name}}
+ {% if values | len > 20 %}
+
+
+ {% endif %}
+
+ {% if attribute.item_attribute_values %}
+
+ {% for attr_value in attribute.item_attribute_values %}
+
+
+
+ {{ attr_value }}
+
+
+ {% endfor %}
+
+ {% else %}
+
{{ _('No values') }}
+ {% endif %}
+
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro recommended_item_row(item)-%}
+
+
+ {% if item.website_item_thumbnail %}
+ {{ product_image(item.website_item_thumbnail, css_class="r-product-image", alt="item.website_item_name", no_border=True) }}
+ {% else %}
+
+ {{ frappe.utils.get_abbr(item.website_item_name) or "NA" }}
+
+ {% endif %}
+
+
+
{%- endmacro -%}
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 291220629c..327552117b 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -6,7 +6,17 @@
-
+
-
+
+ {% 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
index 7b3c9a4131..3f2c1f2a1d 100644
--- a/erpnext/templates/includes/order/order_macros.html
+++ b/erpnext/templates/includes/order/order_macros.html
@@ -1,43 +1,49 @@
-{% from "erpnext/templates/includes/macros.html" import product_image_square %}
+{% from "erpnext/templates/includes/macros.html" import product_image %}
{% macro item_name_and_description(d) %}
-
-
-
- {{ product_image_square(d.thumbnail or d.image) }}
-
-
-
- {{ d.item_code }}
-
+
+
+
+ {% 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) }}
-
-
+
+
{% endmacro %}
{% macro item_name_and_description_cart(d) %}
-
-
-
- {{ product_image_square(d.thumbnail or d.image) }}
-
-
-
- {{ d.item_name|truncate(25) }}
-
+
+ {{ d.item_name|truncate(25) }}
+
+
+
+ –
+
+
+
+
+ +
+
+
+
+
{% endmacro %}
diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html
index d2c458e0a4..b821e6253d 100644
--- a/erpnext/templates/includes/order/order_taxes.html
+++ b/erpnext/templates/includes/order/order_taxes.html
@@ -1,9 +1,9 @@
{% if doc.taxes %}
-
+
{{ _("Net Total") }}
-
+
{{ doc.get_formatted("net_total") }}
@@ -12,10 +12,10 @@
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
-
+
{{ d.description }}
-
+
{{ d.get_formatted("base_tax_amount") }}
@@ -23,76 +23,62 @@
{% endfor %}
{% if doc.doctype == 'Quotation' %}
-{% if doc.coupon_code %}
-
-
- {{ _("Discount") }}
-
-
- {% set tot_quotation_discount = [] %}
- {%- for item in doc.items -%}
- {% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
- * item.discount_percentage) / 100)) %}{% endif %}
- {% endfor %}
- {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
-
-
-{% endif %}
+ {% if doc.coupon_code %}
+
+
+ {{ _("Savings") }}
+
+
+ {% set tot_quotation_discount = [] %}
+ {%- for item in doc.items -%}
+ {% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
+ * item.discount_percentage) / 100)) %}
+ {% endif %}
+ {% endfor %}
+ {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
+
+
+ {% endif %}
{% endif %}
{% if doc.doctype == 'Sales Order' %}
-{% if doc.coupon_code %}
-
-
- {{ _("Total Amount") }}
-
-
-
- {% set total_amount = [] %}
- {%- for item in doc.items -%}
- {% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
- {% endfor %}
- {{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
-
-
-
-
-
- {{ _("Applied Coupon Code") }}
-
-
-
- {%- for row in frappe.get_all(doctype="Coupon Code",
- fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
- {{ row.coupon_code }}
- {% endfor %}
-
-
-
-
-
- {{ _("Discount") }}
-
-
-
- {% set tot_SO_discount = [] %}
- {%- for item in doc.items -%}
- {% if tot_SO_discount.append((((item.price_list_rate * item.qty)
- * item.discount_percentage) / 100)) %}{% endif %}
- {% endfor %}
- {{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
-
-
-
-{% endif %}
+ {% if doc.coupon_code %}
+
+
+ {{ _("Applied Coupon Code") }}
+
+
+
+ {%- for row in frappe.get_all(doctype="Coupon Code",
+ fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
+ {{ row.coupon_code }}
+ {% endfor %}
+
+
+
+
+
+ {{ _("Savings") }}
+
+
+
+ {% set tot_SO_discount = [] %}
+ {%- for item in doc.items -%}
+ {% if tot_SO_discount.append((((item.price_list_rate * item.qty)
+ * item.discount_percentage) / 100)) %}{% endif %}
+ {% endfor %}
+ {{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
+
+
+
+ {% endif %}
{% endif %}
-
-
+
{{ _("Grand Total") }}
-
+
{{ doc.get_formatted("grand_total") }}
diff --git a/erpnext/templates/includes/product_page.js b/erpnext/templates/includes/product_page.js
index 90a1d862c3..a3979d037b 100644
--- a/erpnext/templates/includes/product_page.js
+++ b/erpnext/templates/includes/product_page.js
@@ -7,7 +7,7 @@ frappe.ready(function() {
frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.product_info.get_product_info_for_website",
+ method: "erpnext.e_commerce.shopping_cart.product_info.get_product_info_for_website",
args: {
item_code: get_item_code()
},
diff --git a/erpnext/templates/includes/products_as_list.html b/erpnext/templates/includes/products_as_list.html
index 9bf9fd95d7..a9369bb8de 100644
--- a/erpnext/templates/includes/products_as_list.html
+++ b/erpnext/templates/includes/products_as_list.html
@@ -1,5 +1,5 @@
-{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
-
+{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body, product_image_square %}
+
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
index c64c6343cc..2b7d9e3523 100644
--- a/erpnext/templates/pages/cart.html
+++ b/erpnext/templates/pages/cart.html
@@ -4,13 +4,6 @@
{% block header %}