├── .github └── workflows │ ├── ci.yml │ └── semgroup-rules.yml ├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── webshop.png └── webshop ├── __init__.py ├── config └── __init__.py ├── hooks.py ├── modules.txt ├── patches.txt ├── patches ├── __init__.py ├── add_homepage_field.py ├── clear_cache_for_item_group_route.py ├── convert_to_website_item_in_item_card_group_template.py ├── copy_custom_field_filters_to_website_item.py ├── create_website_items.py ├── enable_allow_to_guest_view_for_item_group.py ├── fetch_thumbnail_in_website_items.py ├── make_homepage_products_website_items.py ├── populate_e_commerce_settings.py └── shopping_cart_to_ecommerce.py ├── public ├── .gitkeep ├── images │ └── cart-empty-state.png ├── js │ ├── customer_reviews.js │ ├── init.js │ ├── override │ │ ├── homepage.js │ │ └── item.js │ ├── product_ui │ │ ├── grid.js │ │ ├── list.js │ │ ├── search.js │ │ └── views.js │ ├── shopping_cart.js │ └── wishlist.js ├── scss │ ├── webshop-web.bundle.scss │ └── webshop_cart.scss └── web.bundle.js ├── setup └── install.py ├── templates ├── __init__.py ├── generators │ ├── item │ │ ├── item.html │ │ ├── item_add_to_cart.html │ │ ├── item_configure.html │ │ ├── item_configure.js │ │ ├── item_details.html │ │ ├── item_image.html │ │ ├── item_inquiry.html │ │ ├── item_inquiry.js │ │ ├── item_reviews.html │ │ └── item_specifications.html │ └── item_group.html ├── includes │ ├── cart │ │ ├── address_card.html │ │ ├── address_picker_card.html │ │ ├── cart_address.html │ │ ├── cart_address_picker.html │ │ ├── cart_dropdown.html │ │ ├── cart_items.html │ │ ├── cart_items_dropdown.html │ │ ├── cart_items_total.html │ │ ├── cart_macros.html │ │ ├── cart_payment_summary.html │ │ ├── coupon_code.html │ │ └── place_order.html │ ├── macros.html │ ├── navbar │ │ └── navbar_items.html │ ├── order │ │ ├── order_macros.html │ │ └── order_taxes.html │ └── product_page.js └── pages │ ├── __init__.py │ ├── cart.html │ ├── cart.js │ ├── cart.py │ ├── customer_reviews.html │ ├── customer_reviews.py │ ├── order.html │ ├── order.js │ ├── order.py │ ├── product_search.html │ ├── product_search.py │ ├── wishlist.html │ └── wishlist.py ├── webshop ├── __init__.py ├── api.py ├── crud_events │ ├── __init__.py │ ├── item │ │ ├── __init__.py │ │ ├── invalidate_item_variants_cache.py │ │ ├── update_website_item.py │ │ └── validate_duplicate_website_item.py │ ├── price_list │ │ ├── __init__.py │ │ └── check_impact_on_cart.py │ ├── quotation │ │ ├── __init__.py │ │ └── validate_shopping_cart_items.py │ └── tax_rule │ │ ├── __init__.py │ │ └── validate_use_for_cart.py ├── doctype │ ├── __init__.py │ ├── homepage_featured_product │ │ ├── __init__.py │ │ ├── homepage_featured_product.json │ │ └── homepage_featured_product.py │ ├── item_review │ │ ├── __init__.py │ │ ├── item_review.js │ │ ├── item_review.json │ │ ├── item_review.py │ │ └── test_item_review.py │ ├── override_doctype │ │ ├── __init__.py │ │ ├── item.py │ │ ├── item_group.py │ │ └── payment_request.py │ ├── recommended_items │ │ ├── __init__.py │ │ ├── recommended_items.json │ │ └── recommended_items.py │ ├── webshop_settings │ │ ├── __init__.py │ │ ├── test_webshop_settings.py │ │ ├── webshop_settings.js │ │ ├── webshop_settings.json │ │ └── webshop_settings.py │ ├── website_item │ │ ├── __init__.py │ │ ├── templates │ │ │ ├── website_item.html │ │ │ └── website_item_row.html │ │ ├── test_website_item.py │ │ ├── website_item.js │ │ ├── website_item.json │ │ ├── website_item.py │ │ └── website_item_list.js │ ├── website_item_tabbed_section │ │ ├── __init__.py │ │ ├── website_item_tabbed_section.json │ │ └── website_item_tabbed_section.py │ ├── website_offer │ │ ├── __init__.py │ │ ├── website_offer.json │ │ └── website_offer.py │ ├── wishlist │ │ ├── __init__.py │ │ ├── test_wishlist.py │ │ ├── wishlist.js │ │ ├── wishlist.json │ │ └── wishlist.py │ └── wishlist_item │ │ ├── __init__.py │ │ ├── wishlist_item.json │ │ └── wishlist_item.py ├── legacy_search.py ├── product_data_engine │ ├── filters.py │ ├── query.py │ ├── test_item_group_product_data_engine.py │ └── test_product_data_engine.py ├── redisearch_utils.py ├── shopping_cart │ ├── __init__.py │ ├── cart.py │ ├── product_info.py │ ├── test_shopping_cart.py │ └── utils.py ├── utils │ ├── __init__.py │ ├── portal.py │ ├── product.py │ └── setup.py ├── variant_selector │ ├── __init__.py │ ├── item_variants_cache.py │ ├── test_variant_selector.py │ └── utils.py └── web_template │ ├── __init__.py │ ├── hero_slider │ ├── __init__.py │ ├── hero_slider.html │ └── hero_slider.json │ ├── item_card_group │ ├── __init__.py │ ├── item_card_group.html │ └── item_card_group.json │ ├── product_card │ ├── __init__.py │ ├── product_card.html │ └── product_card.json │ └── product_category_cards │ ├── __init__.py │ ├── product_category_cards.html │ └── product_category_cards.json └── www ├── __init__.py ├── all-products ├── __init__.py ├── index.html ├── index.js ├── index.py └── not_found.html └── shop-by-category ├── __init__.py ├── category_card_section.html ├── index.html ├── index.js └── index.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "**.css" 7 | - "**.js" 8 | - "**.md" 9 | - "**.html" 10 | - "**.csv" 11 | schedule: 12 | # Run everday at midnight UTC / 5:30 IST 13 | - cron: "0 0 * * *" 14 | 15 | env: 16 | WEBSHOP_BRANCH: ${{ github.base_ref || github.ref_name }} 17 | 18 | concurrency: 19 | group: develop-webshop-${{ github.event.number }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | tests: 24 | runs-on: ubuntu-latest 25 | strategy: 26 | fail-fast: false 27 | name: Server 28 | 29 | services: 30 | redis-cache: 31 | image: redis:alpine 32 | ports: 33 | - 13000:6379 34 | redis-queue: 35 | image: redis:alpine 36 | ports: 37 | - 11000:6379 38 | mariadb: 39 | image: mariadb:10.6 40 | env: 41 | MYSQL_ROOT_PASSWORD: root 42 | ports: 43 | - 3306:3306 44 | options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 45 | 46 | steps: 47 | - name: Clone 48 | uses: actions/checkout@v3 49 | 50 | - name: Find tests 51 | run: | 52 | echo "Finding tests" 53 | grep -rn "def test" > /dev/null 54 | 55 | - name: Setup Python 56 | uses: actions/setup-python@v4 57 | with: 58 | python-version: '3.10' 59 | 60 | - name: Setup Node 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: 18 64 | check-latest: true 65 | 66 | - name: Cache pip 67 | uses: actions/cache@v4 68 | with: 69 | path: ~/.cache/pip 70 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} 71 | restore-keys: | 72 | ${{ runner.os }}-pip- 73 | ${{ runner.os }}- 74 | 75 | - name: Get yarn cache directory path 76 | id: yarn-cache-dir-path 77 | run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' 78 | 79 | - uses: actions/cache@v4 80 | id: yarn-cache 81 | with: 82 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 83 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 84 | restore-keys: | 85 | ${{ runner.os }}-yarn- 86 | 87 | - name: Install MariaDB Client 88 | run: sudo apt update && sudo apt-get install mariadb-client 89 | 90 | - name: Setup 91 | run: | 92 | pip install frappe-bench 93 | bench init --frappe-branch $WEBSHOP_BRANCH --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench 94 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" 95 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 96 | 97 | - name: Install 98 | working-directory: /home/runner/frappe-bench 99 | run: | 100 | bench setup requirements --dev 101 | bench get-app erpnext --branch $WEBSHOP_BRANCH 102 | bench get-app payments --branch $WEBSHOP_BRANCH 103 | bench get-app $GITHUB_WORKSPACE 104 | bench new-site --db-root-password root --admin-password admin test_site 105 | bench --site test_site install-app erpnext 106 | bench --site test_site install-app webshop 107 | bench build 108 | env: 109 | CI: 'Yes' 110 | 111 | - name: Run Tests 112 | working-directory: /home/runner/frappe-bench 113 | run: | 114 | bench --site test_site set-config allow_tests true 115 | bench --site test_site run-tests --app webshop 116 | env: 117 | TYPE: server -------------------------------------------------------------------------------- /.github/workflows/semgroup-rules.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | pull_request: { } 5 | 6 | jobs: 7 | linters: 8 | name: Frappe Linter 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.10' 17 | 18 | - name: Download Semgrep rules 19 | run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules 20 | 21 | - name: Download semgrep 22 | run: pip install semgrep 23 | 24 | - name: Run Semgrep rules 25 | run: semgrep ci --config ./frappe-semgrep-rules/rules 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | *.egg-info 3 | *.pyc 4 | *.py~ 5 | *.swo 6 | *.swp 7 | *~ 8 | .DS_Store 9 | .backportrc.json 10 | .idea/ 11 | .vscode/ 12 | .wnf-lang-status 13 | __pycache__ 14 | conf.py 15 | dist/ 16 | webshop/docs/current 17 | webshop/public/dist 18 | latest_updates.json 19 | locale 20 | node_modules/ 21 | tags 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "webshop" 3 | authors = [ 4 | { name = "Frappe Technologies Pvt. Ltd.", email = "contact@frappe.io"} 5 | ] 6 | description = "Open Source eCommerce Platform" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [] 11 | 12 | [build-system] 13 | requires = ["flit_core >=3.4,<4"] 14 | build-backend = "flit_core.buildapi" 15 | 16 | [tool.black] 17 | line-length = 99 18 | 19 | [tool.isort] 20 | line_length = 99 21 | multi_line_output = 3 22 | include_trailing_comma = true 23 | force_grid_wrap = 0 24 | use_parentheses = true 25 | ensure_newline_before_comments = true 26 | indent = "\t" 27 | -------------------------------------------------------------------------------- /webshop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/webshop/5a1c9e860d4c586279a3825ff05e0d4ffba6ab06/webshop.png -------------------------------------------------------------------------------- /webshop/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' 3 | 4 | -------------------------------------------------------------------------------- /webshop/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/webshop/5a1c9e860d4c586279a3825ff05e0d4ffba6ab06/webshop/config/__init__.py -------------------------------------------------------------------------------- /webshop/hooks.py: -------------------------------------------------------------------------------- 1 | from . import __version__ as _version 2 | 3 | app_name = "webshop" 4 | app_title = "Webshop" 5 | app_publisher = "Frappe Technologies Pvt. Ltd." 6 | app_description = "Open Source eCommerce Platform" 7 | app_email = "contact@frappe.io" 8 | app_license = "GNU General Public License (v3)" 9 | app_version = _version 10 | 11 | required_apps = ["payments", "erpnext"] 12 | 13 | web_include_css = "webshop-web.bundle.css" 14 | 15 | web_include_js = "web.bundle.js" 16 | 17 | after_install = "webshop.setup.install.after_install" 18 | on_logout = "webshop.webshop.shopping_cart.utils.clear_cart_count" 19 | on_session_creation = [ 20 | "webshop.webshop.utils.portal.update_debtors_account", 21 | "webshop.webshop.shopping_cart.utils.set_cart_count", 22 | ] 23 | update_website_context = [ 24 | "webshop.webshop.shopping_cart.utils.update_website_context", 25 | ] 26 | 27 | website_generators = ["Website Item", "Item Group"] 28 | 29 | override_doctype_class = { 30 | "Payment Request": "webshop.webshop.doctype.override_doctype.payment_request.PaymentRequest", 31 | "Item Group": "webshop.webshop.doctype.override_doctype.item_group.WebshopItemGroup", 32 | "Item": "webshop.webshop.doctype.override_doctype.item.WebshopItem", 33 | } 34 | 35 | doctype_js = { 36 | "Item": "public/js/override/item.js", 37 | "Homepage": "public/js/override/homepage.js", 38 | } 39 | 40 | doc_events = { 41 | "Item": { 42 | "on_update": [ 43 | "webshop.webshop.crud_events.item.update_website_item.execute", 44 | "webshop.webshop.crud_events.item.invalidate_item_variants_cache.execute", 45 | ], 46 | "before_rename": [ 47 | "webshop.webshop.crud_events.item.validate_duplicate_website_item.execute", 48 | ], 49 | "after_rename": [ 50 | "webshop.webshop.crud_events.item.invalidate_item_variants_cache.execute", 51 | ], 52 | }, 53 | "Sales Taxes and Charges Template": { 54 | "on_update": [ 55 | "webshop.webshop.doctype.webshop_settings.webshop_settings.validate_cart_settings", 56 | ], 57 | }, 58 | "Quotation": { 59 | "validate": [ 60 | "webshop.webshop.crud_events.quotation.validate_shopping_cart_items.execute", 61 | ], 62 | }, 63 | "Price List": { 64 | "validate": [ 65 | "webshop.webshop.crud_events.price_list.check_impact_on_cart.execute" 66 | ], 67 | }, 68 | "Tax Rule": { 69 | "validate": [ 70 | "webshop.webshop.crud_events.tax_rule.validate_use_for_cart.execute", 71 | ], 72 | }, 73 | } 74 | 75 | has_website_permission = { 76 | "Website Item": "webshop.webshop.doctype.website_item.website_item.has_website_permission_for_website_item", 77 | "Item Group": "webshop.webshop.doctype.website_item.website_item.has_website_permission_for_item_group" 78 | } 79 | -------------------------------------------------------------------------------- /webshop/modules.txt: -------------------------------------------------------------------------------- 1 | Webshop -------------------------------------------------------------------------------- /webshop/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | 3 | [post_model_sync] 4 | 5 | webshop.patches.add_homepage_field #09-05-2024 6 | webshop.patches.enable_allow_to_guest_view_for_item_group 7 | webshop.patches.clear_cache_for_item_group_route -------------------------------------------------------------------------------- /webshop/patches/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' 3 | 4 | -------------------------------------------------------------------------------- /webshop/patches/add_homepage_field.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields 3 | 4 | 5 | def execute(): 6 | if not frappe.db.exists("DocType", "Homepage"): 7 | return 8 | if not frappe.db.exists("Custom Field", {"fieldname": "products", "dt": "Homepage"}): 9 | custom_fields = { 10 | "Homepage": [ 11 | dict( 12 | fieldname="products_section_break", 13 | label="Products", 14 | fieldtype="Section Break", 15 | insert_after="hero_section", 16 | ), 17 | dict( 18 | fieldname="products_url", 19 | label="URL for All Products", 20 | fieldtype="Data", 21 | insert_after="products_section_break", 22 | ), 23 | dict( 24 | fieldname="products", 25 | label="Products", 26 | fieldtype="Table", 27 | insert_after="products_url", 28 | options="Homepage Featured Product", 29 | ), 30 | ], 31 | } 32 | 33 | create_custom_fields(custom_fields) 34 | -------------------------------------------------------------------------------- /webshop/patches/clear_cache_for_item_group_route.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.website.utils import clear_cache 3 | 4 | def execute(): 5 | routes = frappe.get_all("Item Group", filters={"show_in_website": 1, "route": ("is", "set")}, pluck="route") 6 | for route in routes: 7 | clear_cache(route) -------------------------------------------------------------------------------- /webshop/patches/convert_to_website_item_in_item_card_group_template.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List, Union 3 | 4 | import frappe 5 | 6 | from webshop.webshop.doctype.website_item.website_item import make_website_item 7 | 8 | 9 | def execute(): 10 | """ 11 | Convert all Item links to Website Item link values in 12 | exisitng 'Item Card Group' Web Page Block data. 13 | """ 14 | frappe.reload_doc("webshop", "web_template", "item_card_group") 15 | 16 | blocks = frappe.db.get_all( 17 | "Web Page Block", 18 | filters={"web_template": "Item Card Group"}, 19 | fields=["parent", "web_template_values", "name"], 20 | ) 21 | 22 | fields = generate_fields_to_edit() 23 | 24 | for block in blocks: 25 | web_template_value = json.loads(block.get("web_template_values")) 26 | 27 | for field in fields: 28 | item = web_template_value.get(field) 29 | if not item: 30 | continue 31 | 32 | if frappe.db.exists("Website Item", {"item_code": item}): 33 | website_item = frappe.db.get_value("Website Item", {"item_code": item}) 34 | else: 35 | website_item = make_new_website_item(item) 36 | 37 | if website_item: 38 | web_template_value[field] = website_item 39 | 40 | frappe.db.set_value( 41 | "Web Page Block", block.name, "web_template_values", json.dumps(web_template_value) 42 | ) 43 | 44 | 45 | def generate_fields_to_edit() -> List: 46 | fields = [] 47 | for i in range(1, 13): 48 | fields.append(f"card_{i}_item") # fields like 'card_1_item', etc. 49 | 50 | return fields 51 | 52 | 53 | def make_new_website_item(item: str) -> Union[str, None]: 54 | try: 55 | doc = frappe.get_doc("Item", item) 56 | web_item = make_website_item(doc) # returns [website_item.name, item_name] 57 | return web_item[0] 58 | except Exception: 59 | doc.log_error("Website Item creation failed") 60 | return None -------------------------------------------------------------------------------- /webshop/patches/copy_custom_field_filters_to_website_item.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.custom.doctype.custom_field.custom_field import create_custom_field 3 | 4 | from webshop.webshop.utils.setup import has_ecommerce_fields 5 | 6 | def execute(): 7 | "Add Field Filters, that are not standard fields in Website Item, as Custom Fields." 8 | 9 | def move_table_multiselect_data(docfield): 10 | "Copy child table data (Table Multiselect) from Item to Website Item for a docfield." 11 | table_multiselect_data = get_table_multiselect_data(docfield) 12 | field = docfield.fieldname 13 | 14 | for row in table_multiselect_data: 15 | # add copied multiselect data rows in Website Item 16 | web_item = frappe.db.get_value("Website Item", {"item_code": row.parent}) 17 | web_item_doc = frappe.get_doc("Website Item", web_item) 18 | 19 | child_doc = frappe.new_doc(docfield.options, parent_doc=web_item_doc, parentfield=field) 20 | 21 | for field in ["name", "creation", "modified", "idx"]: 22 | row[field] = None 23 | 24 | child_doc.update(row) 25 | 26 | child_doc.parenttype = "Website Item" 27 | child_doc.parent = web_item 28 | 29 | child_doc.insert() 30 | 31 | def get_table_multiselect_data(docfield): 32 | child_table = frappe.qb.DocType(docfield.options) 33 | item = frappe.qb.DocType("Item") 34 | 35 | table_multiselect_data = ( # query table data for field 36 | frappe.qb.from_(child_table) 37 | .join(item) 38 | .on(item.item_code == child_table.parent) 39 | .select(child_table.star) 40 | .where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1)) 41 | ).run(as_dict=True) 42 | 43 | return table_multiselect_data 44 | 45 | settings_doctype = "E Commerce Settings" if has_ecommerce_fields() else "Webshop Settings" 46 | 47 | settings = frappe.get_doc(settings_doctype) 48 | 49 | if not (settings.enable_field_filters or settings.filter_fields): 50 | return 51 | 52 | item_meta = frappe.get_meta("Item") 53 | valid_item_fields = [ 54 | df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] 55 | ] 56 | 57 | web_item_meta = frappe.get_meta("Website Item") 58 | valid_web_item_fields = [ 59 | df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] 60 | ] 61 | 62 | for row in settings.filter_fields: 63 | # skip if illegal field 64 | if row.fieldname not in valid_item_fields: 65 | continue 66 | 67 | # if Item field is not in Website Item, add it as a custom field 68 | if row.fieldname not in valid_web_item_fields: 69 | df = item_meta.get_field(row.fieldname) 70 | create_custom_field( 71 | "Website Item", 72 | dict( 73 | owner="Administrator", 74 | fieldname=df.fieldname, 75 | label=df.label, 76 | fieldtype=df.fieldtype, 77 | options=df.options, 78 | description=df.description, 79 | read_only=df.read_only, 80 | no_copy=df.no_copy, 81 | insert_after="on_backorder", 82 | ), 83 | ) 84 | 85 | # map field values 86 | if df.fieldtype == "Table MultiSelect": 87 | move_table_multiselect_data(df) 88 | else: 89 | frappe.db.sql( # nosemgrep 90 | """ 91 | UPDATE `tabWebsite Item` wi, `tabItem` i 92 | SET wi.{0} = i.{0} 93 | WHERE wi.item_code = i.item_code 94 | """.format( 95 | row.fieldname 96 | ) 97 | ) -------------------------------------------------------------------------------- /webshop/patches/create_website_items.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | from webshop.webshop.doctype.website_item.website_item import make_website_item 4 | 5 | 6 | def execute(): 7 | if frappe.get_all("Website Item", limit=1): 8 | return 9 | 10 | frappe.reload_doc("webshop", "doctype", "website_item") 11 | frappe.reload_doc("webshop", "doctype", "website_item_tabbed_section") 12 | frappe.reload_doc("webshop", "doctype", "website_offer") 13 | frappe.reload_doc("webshop", "doctype", "recommended_items") 14 | frappe.reload_doc("webshop", "doctype", "webshop_settings") 15 | frappe.reload_doc("stock", "doctype", "item") 16 | 17 | item_fields = [ 18 | "item_code", 19 | "item_name", 20 | "item_group", 21 | "stock_uom", 22 | "brand", 23 | "has_variants", 24 | "variant_of", 25 | "description", 26 | "weightage", 27 | ] 28 | web_fields_to_map = [ 29 | "route", 30 | "slideshow", 31 | "website_image_alt", 32 | "website_warehouse", 33 | "web_long_description", 34 | "website_content", 35 | "website_image", 36 | "thumbnail", 37 | ] 38 | 39 | # get all valid columns (fields) from Item master DB schema 40 | item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) # nosemgrep 41 | item_table_fields = [d.get("Field") for d in item_table_fields] 42 | 43 | # prepare fields to query from Item, check if the web field exists in Item master 44 | web_query_fields = [] 45 | for web_field in web_fields_to_map: 46 | if web_field in item_table_fields: 47 | web_query_fields.append(web_field) 48 | item_fields.append(web_field) 49 | 50 | # check if the filter fields exist in Item master 51 | or_filters = {} 52 | for field in ["show_in_website", "show_variant_in_website"]: 53 | if field in item_table_fields: 54 | or_filters[field] = 1 55 | 56 | if not web_query_fields or not or_filters: 57 | # web fields to map are not present in Item master schema 58 | # most likely a fresh installation that doesnt need this patch 59 | return 60 | 61 | items = frappe.db.get_all("Item", fields=item_fields, or_filters=or_filters) 62 | total_count = len(items) 63 | 64 | for count, item in enumerate(items, start=1): 65 | if frappe.db.exists("Website Item", {"item_code": item.item_code}): 66 | continue 67 | 68 | # make new website item from item (publish item) 69 | website_item = make_website_item(item, save=False) 70 | website_item.ranking = item.get("weightage") 71 | 72 | for field in web_fields_to_map: 73 | website_item.update({field: item.get(field)}) 74 | 75 | website_item.save() 76 | 77 | # move Website Item Group & Website Specification table to Website Item 78 | for doctype in ("Website Item Group", "Item Website Specification"): 79 | frappe.db.set_value( 80 | doctype, 81 | {"parenttype": "Item", "parent": item.item_code}, # filters 82 | {"parenttype": "Website Item", "parent": website_item.name}, # value dict 83 | ) 84 | 85 | if count % 20 == 0: # commit after every 20 items 86 | frappe.db.commit() 87 | 88 | frappe.utils.update_progress_bar("Creating Website Items", count, total_count) 89 | -------------------------------------------------------------------------------- /webshop/patches/enable_allow_to_guest_view_for_item_group.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.custom.doctype.property_setter.property_setter import make_property_setter 3 | 4 | def execute(): 5 | frappe.reload_doc("setup", "doctype", "item_group") 6 | 7 | make_property_setter("Item Group", "", "has_web_view", 1, "Check", for_doctype=True, validate_fields_for_doctype=False) 8 | make_property_setter("Item Group", "", "allow_guest_to_view", 1, "Check", for_doctype=True, validate_fields_for_doctype=False) 9 | -------------------------------------------------------------------------------- /webshop/patches/fetch_thumbnail_in_website_items.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | if frappe.db.has_column("Item", "thumbnail"): 6 | website_item = frappe.qb.DocType("Website Item").as_("wi") 7 | item = frappe.qb.DocType("Item") 8 | 9 | frappe.qb.update(website_item).inner_join(item).on(website_item.item_code == item.item_code).set( 10 | website_item.thumbnail, item.thumbnail 11 | ).where(website_item.website_image.notnull() & website_item.thumbnail.isnull()).run() -------------------------------------------------------------------------------- /webshop/patches/make_homepage_products_website_items.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def execute(): 5 | if not frappe.db.exists("DocType", "Homepage"): 6 | return 7 | homepage = frappe.get_doc("Homepage") 8 | 9 | for row in homepage.products: 10 | web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name") 11 | if not web_item: 12 | continue 13 | 14 | row.item_code = web_item 15 | 16 | homepage.flags.ignore_mandatory = True 17 | homepage.save() -------------------------------------------------------------------------------- /webshop/patches/populate_e_commerce_settings.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.utils import cint 3 | 4 | from webshop.webshop.utils.setup import has_ecommerce_fields 5 | 6 | def execute(): 7 | frappe.reload_doc("webshop", "doctype", "webshop_settings") 8 | frappe.reload_doc("portal", "doctype", "website_filter_field") 9 | frappe.reload_doc("portal", "doctype", "website_attribute") 10 | 11 | products_settings_fields = [ 12 | "hide_variants", 13 | "products_per_page", 14 | "enable_attribute_filters", 15 | "enable_field_filters", 16 | ] 17 | 18 | shopping_cart_settings_fields = [ 19 | "enabled", 20 | "show_attachments", 21 | "show_price", 22 | "show_stock_availability", 23 | "enable_variants", 24 | "show_contact_us_button", 25 | "show_quantity_in_website", 26 | "show_apply_coupon_code_in_website", 27 | "allow_items_not_in_stock", 28 | "company", 29 | "price_list", 30 | "default_customer_group", 31 | "quotation_series", 32 | "enable_checkout", 33 | "payment_success_url", 34 | "payment_gateway_account", 35 | "save_quotations_as_draft", 36 | ] 37 | 38 | settings_doctype = "E Commerce Settings" if has_ecommerce_fields() else "Webshop Settings" 39 | 40 | settings = frappe.get_doc(settings_doctype) 41 | 42 | def map_into_e_commerce_settings(doctype, fields): 43 | singles = frappe.qb.DocType("Singles") 44 | query = ( 45 | frappe.qb.from_(singles) 46 | .select(singles["field"], singles.value) 47 | .where((singles.doctype == doctype) & (singles["field"].isin(fields))) 48 | ) 49 | data = query.run(as_dict=True) 50 | 51 | # {'enable_attribute_filters': '1', ...} 52 | mapper = {row.field: row.value for row in data} 53 | 54 | for key, value in mapper.items(): 55 | value = cint(value) if (value and value.isdigit()) else value 56 | settings.update({key: value}) 57 | 58 | settings.save() 59 | 60 | # shift data to E Commerce Settings 61 | map_into_e_commerce_settings("Products Settings", products_settings_fields) 62 | map_into_e_commerce_settings("Shopping Cart Settings", shopping_cart_settings_fields) 63 | 64 | # move filters and attributes tables to E Commerce Settings from Products Settings 65 | for doctype in ("Website Filter Field", "Website Attribute"): 66 | frappe.db.set_value( 67 | doctype, 68 | {"parent": "Products Settings"}, 69 | {"parenttype": settings_doctype, "parent": settings_doctype}, 70 | update_modified=False, 71 | ) -------------------------------------------------------------------------------- /webshop/patches/shopping_cart_to_ecommerce.py: -------------------------------------------------------------------------------- 1 | import click 2 | import frappe 3 | 4 | 5 | def execute(): 6 | 7 | frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True) 8 | frappe.delete_doc("DocType", "Products Settings", ignore_missing=True) 9 | frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True) 10 | -------------------------------------------------------------------------------- /webshop/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/webshop/5a1c9e860d4c586279a3825ff05e0d4ffba6ab06/webshop/public/.gitkeep -------------------------------------------------------------------------------- /webshop/public/images/cart-empty-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/webshop/5a1c9e860d4c586279a3825ff05e0d4ffba6ab06/webshop/public/images/cart-empty-state.png -------------------------------------------------------------------------------- /webshop/public/js/customer_reviews.js: -------------------------------------------------------------------------------- 1 | $(() => { 2 | class CustomerReviews { 3 | constructor() { 4 | this.bind_button_actions(); 5 | this.start = 0; 6 | this.page_length = 10; 7 | } 8 | 9 | bind_button_actions() { 10 | this.write_review(); 11 | this.view_more(); 12 | } 13 | 14 | write_review() { 15 | //TODO: make dialog popup on stray page 16 | $('.page_content').on('click', '.btn-write-review', (e) => { 17 | // Bind action on write a review button 18 | const $btn = $(e.currentTarget); 19 | 20 | let d = new frappe.ui.Dialog({ 21 | title: __("Write a Review"), 22 | fields: [ 23 | {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1}, 24 | {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1}, 25 | {fieldtype: "Section Break"}, 26 | {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"} 27 | ], 28 | primary_action: function() { 29 | let data = d.get_values(); 30 | frappe.call({ 31 | method: "webshop.webshop.doctype.item_review.item_review.add_item_review", 32 | args: { 33 | web_item: $btn.attr('data-web-item'), 34 | title: data.title, 35 | rating: data.rating, 36 | comment: data.comment 37 | }, 38 | freeze: true, 39 | freeze_message: __("Submitting Review ..."), 40 | callback: (r) => { 41 | if (!r.exc) { 42 | frappe.msgprint({ 43 | message: __("Thank you for submitting your review"), 44 | title: __("Review Submitted"), 45 | indicator: "green" 46 | }); 47 | d.hide(); 48 | location.reload(); 49 | } 50 | } 51 | }); 52 | }, 53 | primary_action_label: __("Submit") 54 | }); 55 | d.show(); 56 | }); 57 | } 58 | 59 | view_more() { 60 | $('.page_content').on('click', '.btn-view-more', (e) => { 61 | // Bind action on view more button 62 | const $btn = $(e.currentTarget); 63 | $btn.prop('disabled', true); 64 | 65 | this.start += this.page_length; 66 | let me = this; 67 | 68 | frappe.call({ 69 | method: "webshop.webshop.doctype.item_review.item_review.get_item_reviews", 70 | args: { 71 | web_item: $btn.attr('data-web-item'), 72 | start: me.start, 73 | end: me.page_length 74 | }, 75 | callback: (result) => { 76 | if (result.message) { 77 | let res = result.message; 78 | me.get_user_review_html(res.reviews); 79 | 80 | $btn.prop('disabled', false); 81 | if (res.total_reviews <= (me.start + me.page_length)) { 82 | $btn.hide(); 83 | } 84 | 85 | } 86 | } 87 | }); 88 | }); 89 | 90 | } 91 | 92 | get_user_review_html(reviews) { 93 | let me = this; 94 | let $content = $('.user-reviews'); 95 | 96 | reviews.forEach((review) => { 97 | $content.append(` 98 |
101 | ${__(review.review_title)} 102 |
103 | 106 |110 | ${__(review.comment)} 111 |
112 |31 | 32 | {{ _(doc.item_group) }} 33 | 34 | 35 | {{ _("Item Code") }}: 36 | 37 | {{ _(doc.item_code) }} 38 |
39 | {% if has_variants %} 40 | 41 | {% include "templates/generators/item/item_configure.html" %} 42 | {% else %} 43 | 44 | {% include "templates/generators/item/item_add_to_cart.html" %} 45 | {% endif %} 46 | 47 |{{ _(d.label) }} | 14 |{{ _(d.description) }} | 15 |
{{ _("Cart is Empty") }}
26 | {% endif %} 27 |{{ _("Net Total (") + total_items + _(" Items)") }} | 15 |{{ doc.get_formatted("net_total") }} | 16 |
23 | {{ d.description }} 24 | | 25 |26 | {{ d.get_formatted("tax_amount") }} 27 | | 28 |
{{ _("Grand Total") }} | 50 |{{ doc.get_formatted("grand_total") }} | 51 |
Coupon code
3 |{{ _('Item') }} | 28 |{{ _('Quantity') }} | 29 | {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} 30 |{{ _('Subtotal') }} | 31 | {% endif %} 32 |33 | |
---|
{{ _(subtitle) }}
{%- endif -%} 19 | {%- if action -%} 20 | 21 | {{ _(label) }} 22 | 23 | {%- endif -%} 24 |{{ subtitle }}
11 | {%- endif -%} 12 |{{ subtitle }}
31 | {%- endif -%} 32 | 33 |