External Shopping Cart App

We are creating a new Progressive Web App for creating a shopping cart and wanted some ideas/views on the integration with ERPNext.

The app would allow user to browse through the items, add to cart and checkout. There are 2 options on how checkout could be implemented -

1. Maintain items in local cache when user adds item to the cart
2. POST a Sales order in ERPNext on checkout

OR

1. Create a Cart using erpnext.shopping_cart.cart.update_cart API to add item to cart on the ERPNext side
2. Call erpnext.shopping_cart.cart.place_order when user submits the cart.

We realized that if ‘Sales Order’ is POSTed directly then all the calculation of calculating the Tax based on Tax rules and shipping rules, needs to be done beforehand and should be posted as part of the ‘Sales Order’ data.

However if we use the ‘cart’ API instead, the tax calculation happens when the place_order API is called.

I wanted to know from this community on what would be the best way to implement this considering all the tax calculations (in GST and non GST) scenarios.

Moreover, I am getting HTTP 400 error when i call erpnext.shopping_cart.cart.place_order API. I am calling below APIs in sequence -

1. Login 
2. erpnext.shopping_cart.cart.update_cart  -> This creates a new Quotation 
3. erpnext.shopping_cart.cart.place_order  -> This fails with error 400

For 2 way communication you need to have API on both web apps (Frappe/ERPNext and your web app)

To have control over endpoints on ERPNext’s end, you need .py whitelisted api file on frappe server.
via Custom App you can do that.

With custom app you can also add hooks for doc_events, You can use it to create/update/delete items on your web app when doc_events occur on frappe’s end. e.g. Item is created on ERPNext, API for your Web App is fired (reverse api / webhook)

GST is broadly dependent on

  1. Availability of valid GSTIN in Address (B2B, B2C)
  2. Address Linked with Warehouse, Company, Supplier, Customer (identifies IGST/CGST/SGST)
  3. Item Tax and Tax Rules
    refer https://github.com/frappe/frappe/pull/3443 and https://github.com/frappe/erpnext/issues/8711

Print out the error responses from frappe server and you will get the Python Traceback.

Optional :
(this will save 2 api calls to login and logout. Only api call with right token will be enough)

  1. Use full OAuth 2 Feature from Frappe to authenticate into Frappe’s REST API.
  2. Use OAuth 2 Partly, hand out user based OAuth Bearer Token(s) and pass them as Header Authorization: Bearer <token>

Thanks @revant_one

How do we do this? Can you please point me to any documentation?[quote=“revant_one, post:2, topic:24697”]
Use full OAuth 2 Feature from Frappe to authenticate into Frappe’s REST API.
Use OAuth 2 Partly, hand out user based OAuth Bearer Token(s) and pass them as Header Authorization: Bearer <token>
[/quote]

I will try this out.

I was able to resolve the issue with placing the order. It was preventing order setup because of non assignment of delivery warehouse to the item (In the scope of a Company)

This leads to my another question on how to enable “Sales Order” generation for multiple companies? I have one ERPNext site and there are multiple Companies in it. I want to be able host a shopping website for multiple companies.

Multiple companies should share same set of customers and same set of Items. Is it possible?

I can create multiple sites for multiple companies (who want to sell), but is it possible to share underlying Customer and ITem data across sites?

You will have to do that in your application (for debugging purpose).

Python Example

In [1]: import requests

In [2]: r = requests.get("https://demo.erpnext.com/api/resource/test/error")

In [3]: r.text
Out[3]: u'<!DOCTYPE html>\n<!-- Built on Frappe. https://frappe.io/ -->\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1.0">\n\t<title>Server Error</title>\n\t<meta name="generator" content="frappe">\n\n    \n\t<link rel="shortcut icon"\n\t\thref="/assets/erpnext/images/favicon.png"\n\t\ttype="image/x-icon">\n\t<link rel="icon"\n\t\thref="/assets/erpnext/images/favicon.png"\n\t\ttype="image/x-icon">\n    \n\n\t\t\n\t\t<link type="text/css" rel="stylesheet" href="/assets/frappe/css/bootstrap.css">\n\t\t<link type="text/css" rel="stylesheet" href="/assets/css/frappe-web.css">\n\t\t<link type="text/css" rel="stylesheet" href="/assets/erpnext/css/website.css">\n\t\t<link type="text/css" rel="stylesheet" href="/website_theme.css"><script>\n\t\twindow.frappe = {};\n\t\tfrappe.ready_events = [];\n\t\tfrappe.ready = function(fn) {\n\t\t\tfrappe.ready_events.push(fn);\n\t\t}\n\t\twindow.dev_server = 0;\n    </script>\n</head>\n<body data-path="api/resource/test/error">\n\t<div class="main-section">\n\t\t<div>\n\t\t\t<header>\n\t\t\t\t\t<nav class="navbar navbar-default navbar-main" role="navigation">\n\t\t<div class="container">\n\t\t\t<div class="navbar-header">\n\t\t\t\t<a class="navbar-brand ellipsis"\n\t\t\t\t\t href="/">\n\t\t\t\t\t<span>Home</span>\n\t\t\t\t</a>\n\t\t\t\t<div class="dropdown">\n\t\t\t\t\t<button class="btn btn-default navbar-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">\n\t\t\t\t\t\t<i class="octicon octicon-three-bars"></i>\n\t\t\t\t\t</button>\n\t\t\t\t\t<ul class="dropdown-menu dropdown-menu-right">\n\n\n<li class="divider"></li>\n\n<!-- post login tools -->\n<li data-label="My Account" \n class=" logged-in" ><a href="/me" \n\trel="nofollow">\n\tMy Account\n\t</a></li><li data-label="Logout" \n class=" logged-in" ><a href="/?cmd=web_logout" \n\trel="nofollow">\n\tLogout\n\t</a></li>\n<li class="btn-login-area"><a href="/login">Login</a></li>\n\n\n</ul>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class="hidden-xs">\n\t\t\t\t\n\t\t\t\t\n<ul class="nav navbar-nav navbar-right">\n\t\n\t\t\n\t<li class="shopping-cart hidden">\n\t\t<div class="cart-icon">\n\t\t\t<a class="dropdown-toggle" href="#" data-toggle="dropdown" id="navLogin">\n\t\t\t\tCart <span class="badge-wrapper" id="cart-count"></span>\n\t\t\t</a>\n\t\t\t<div id="cart-overlay" class="dropdown-menu shopping-cart-menu"></div>\n\t\t</div>\n\t </li>\n\n\t\n\t\n\t<!-- post login tools -->\n\n<li class="dropdown logged-in" id="website-post-login"\n\tdata-label="website-post-login" style="display: none">\n\t<a href="#" class="dropdown-toggle" data-toggle="dropdown">\n\t\t<span class="user-image-wrapper"></span>\n\t\t<span class="full-name"></span>\n\t\t<b class="caret"></b>\n\t</a>\n    <ul class="dropdown-menu" role="menu"><li data-label="My Account" \n\t\t\t><a href="/me" \n\t\t\t\t\trel="nofollow">\n\t\t\t\t\tMy Account\n\t\t\t\t</a></li><li data-label="Logout" \n\t\t\t><a href="/?cmd=web_logout" \n\t\t\t\t\trel="nofollow">\n\t\t\t\t\tLogout\n\t\t\t\t</a></li></ul>\n</li>\n\n<li class="btn-login-area"><a href="/login">Login</a></li>\n\n\n</ul>\n\t\t\t\t\n\t\t\t</div>\n\t\t</div>\n\t</nav></header>\n\n            <div class="hero-and-content">\n                <div data-html-block="hero"></div>\n                <div class="container">\n                \n<div class="page-container" id="page-message" data-path="desk"\n\t>\n\t<div class="row ">\n\t\t\n\t\t<div class=" page-content col-sm-12 ">\n\t\t\t<div class="page-content-wrapper">\n\t\t\t\t<div class="row page-head">\n\t\t\t\t\t<div class=\'col-sm-12 hidden-xs\'>\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class="col-sm-8 col-xs-6">\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class="col-sm-4 col-xs-6">\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class="page_content">\n\n<div class=\'page-card\'>\n\t<div class=\'page-card-head\'>\n\t\t<span class=\'indicator red\'>\n\t\t\tServer Error</span>\n\t</div>\n\t<p><pre>Traceback (most recent call last):\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 60, in application\n    response = frappe.api.handle()\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/api.py", line 77, in handle\n    doc = frappe.get_doc(doctype, name)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 606, in get_doc\n    return frappe.model.document.get_doc(arg1, arg2)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/model/document.py", line 49, in get_doc\n    controller = get_controller(doctype)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py", line 35, in get_controller\n    module = load_doctype_module(doctype, module_name)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/modules/utils.py", line 184, in load_doctype_module\n    raise ImportError, \'Module import failed for {0} ({1})\'.format(doctype, module_name)\nImportError: Module import failed for test (frappe.core.doctype.test.test)\n</pre></p>\n\t\n\t<div><a href=\'/\' class=\'btn btn-primary btn-sm\'>\n\t\tHome</a></div>\n\t\n</div>\n\n<p class=\'text-muted text-center small\' style=\'margin-top: -20px;\'>Status: 500</p>\n\n<style>\n.hero-and-content {\n\tbackground-color: #f5f7fa;\n}\n\n</style>\n</div>\n\t\t</div>\n\t</div>\n</div>\n\n                </div>\n            </div></div>\n\t\t<div><footer class="web-footer">\n\t<section class="footer-links">\n\t\t<div class="container">\n\t\t\t<div class="row">\n\t\t\t\t<div class="col-sm-6 text-left">\n\t\t\t\t\t\n\t\t\t\t</div>\n\n\t\t\t\t<div class="col-sm-6 text-right">\n\t\t\t\t\t\n\t\t\t\t\t\t\n<div class=\'input-group input-group-sm pull-right footer-subscribe\'>\n\t<input class="form-control" type="text" id="footer-subscribe-email"\n\t\tplaceholder="Your email address...">\n\t<span class=\'input-group-btn\'>\n\t\t<button class="btn btn-default" type="button"\n\t\t\tid="footer-subscribe-button">Get Updates</button>\n\t</span>\n</div>\n\n<script>\nfrappe.ready(function() {\n\t$("#footer-subscribe-button").click(function() {\n\n\t\tif($("#footer-subscribe-email").val() && valid_email($("#footer-subscribe-email").val())) {\n\t\t\t$("#footer-subscribe-email").attr(\'disabled\', true);\n\t\t\t$("#footer-subscribe-button").html("Sending...")\n\t\t\t\t.attr("disabled", true);\n\t\t\terpnext.subscribe_to_newsletter({\n\t\t\t\temail: $("#footer-subscribe-email").val(),\n\t\t\t\tcallback: function(r) {\n\t\t\t\t\tif(!r.exc) {\n\t\t\t\t\t\t$("#footer-subscribe-button").html(__("Added"))\n\t\t\t\t\t\t\t.attr("disabled", true);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$("#footer-subscribe-button").html(__("Error: Not a valid id?"))\n\t\t\t\t\t\t\t.addClass("btn-danger").attr("disabled", false);\n\t\t\t\t\t\t$("#footer-subscribe-email").val("").attr(\'disabled\', false);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\telse\n\t\t\tfrappe.msgprint(frappe._("Please enter valid email address"))\n\t});\n});\n</script>\n\n\n\t\t\t\t\t\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class="row footer-bottom-line">\n\t\t\t\t<div class="text-muted small col-sm-6 col-xs-12">\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</div>\n\t\t\t\t\n\t\t\t\t<div class="text-muted small col-sm-6 col-xs-12\n\t\t\t\t\ttext-right footer-powered">\n\t\t\t\t\t\n\t\t\t\t\t\t<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">\n\t\tPowered by ERPNext</a>\n\t\t\t\t\t\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</section>\n</footer></div>\n\t</div>\n\t\n\t<!-- js should be loaded in body! -->\n\t<script type="text/javascript"\n\t\tsrc="/assets/frappe/js/lib/jquery/jquery.min.js"></script>\n\t<script type="text/javascript"\n\t\tsrc="/assets/js/frappe-web.min.js"></script>\n\t\n\t<script type="text/javascript" src="/website_script.js"></script>\n\t<script type="text/javascript" src="/assets/js/erpnext-web.min.js"></script>\n\n\t<script>frappe.csrf_token = "None";</script></body>\n</html>'

notice the error?

<pre>Traceback (most recent call last):\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 60, in application\n    response = frappe.api.handle()\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/api.py", line 77, in handle\n    doc = frappe.get_doc(doctype, name)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 606, in get_doc\n    return frappe.model.document.get_doc(arg1, arg2)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/model/document.py", line 49, in get_doc\n    controller = get_controller(doctype)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py", line 35, in get_controller\n    module = load_doctype_module(doctype, module_name)\n  File "/home/frappe/frappe-bench/apps/frappe/frappe/modules/utils.py", line 184, in load_doctype_module\n    raise ImportError, \'Module import failed for {0} ({1})\'.format(doctype, module_name)\nImportError: Module import failed for test (frappe.core.doctype.test.test)\n</pre>

@Sachin_Mane, hope you are doing good! is this PWA done…can you share anything on this…it would help …Thanks!