August 07, 2024

Selling with Odoo

Are you a vendor or reseller?
Do you want to place your products onto more virtual shelves?
Let us tell you about our experience with Odoo and Shopify.

Selling online strategy

eshop

The are two main strategies for selling products online, your own E-shop and online marketplace(s).

E-shops are no longer something special or unique. It is par for the course for anyone selling products. Good news is that as e-shops became ubiquitous, so did the solutions that help you open or operate one. Even better some e-shop providers also slap an website onto your e-shop as well.
Unless you have been living in a cave, you probalbly heard of the e-shop provider Shopify.

Marketplaces are of course the household names Amazon, E-bay. But there is a whole plentora of smaller national or local players too.

Why not both?

Recently Shopify acquired the best Amazon & E-bay integration plugin. Thus, if you are looking for covering both strategies fast, your strategy just became use Shopify. You list your products on Shopify and it will list them on marketplaces too, straight from the same app.

How about Odoo

Now this blog is not how and why you should use shopify. This is about our lessons learned.

Our client is using the open source erp Odoo for warehouse&stock management. Previously we have been assisting with integrating selling between Odoo and marketplaces (amazon and ebay). There are odoo addons available, however we always had to go in and tweak or directly change the code of the plugins to work around issues (taxes for example).
Finally, since our customer wanted to open their own eshop and had good experience with Codisto (now called Shopify Marketplace Connect), the decision was made to cut Odoo - marketplace connections and run everything via Shopify.

Unfortunately, there were also issues with the Odoo shopify integration plugin. Let's look at what we had to do yo get stuff running smoothly.

Odoo and Shopify

Shopify data model and api

Shopify has a rather nice and easy to use api. You can find documentation here.

The data model might look a bit complex, especially for smaller vendors who don’t use product variants or multiple stock-locations (warehouses).
Once you imagine it in a example it becomes much clearer. For example let's say your product is a t-shirt with a tiger print. The variants are the colors blue, red and black, or maybe even the sizes S,m,l,xl etc. Remember that customers always buy a particular variant, the product is basically just a way to group the concrete thing people buy together.

Now it should obvious that to indicate stock availability (how many t-shirts you as a vendor have in the warehouse) we need a combination of variant-id and warehouse-id.

Reading products from Shopify

To query the current levels we have in shopify we can use the https://YOUR_ORG.myshopify.com/admin/api/2023-10/products.json?limit=250 api.

def fetch_products():
	hdrs = {'X-Shopify-Access-Token':"YOUR_ACCESS_TOKEN" }
	nextLink = "https://YOUR_ORG.myshopify.com/admin/api/2023-10/products.json?limit=250"
	while nextLink:
		resp = fetch_json(nextLink, None, hdrs, None)

		nextLink:str = resp.info().get("link")
		if nextLink and nextLink.find('rel="next"')<0:
			nextLink = None

		if nextLink:
			p = nextLink.find(', ')
			if p>0:
				nextLink = nextLink[p+2:]
			end = nextLink.find(">;")
			if(end):
				nextLink = nextLink[1:end]

		bytes = resp.read()
		r = json.loads(bytes)

		for p in r['products']:
			for v in p['variants']:
				print(str(v["id"]) + "\t" + p['title'] + "\t" + v['title'] + "\t" + str(v['inventory_quantity']))

def fetch_json(q, payload = None, headers = None, decodeAs = None, encodeAs = None):
	addJsonHeader = False
	data = None
	if payload:
		if not encodeAs or encodeAs == "json":
			data = json.dumps(payload).encode("utf-8")
			addJsonHeader = True
		elif encodeAs == "urlencode":
			data = parse.urlencode(payload).encode()
		else:
			raise Exception("Unsupported encode option:"+encodeAs)
	#ssl._create_default_https_context = ssl._create_unverified_context
	req = request.Request(q, data, method="POST" if data else "GET")

	if addJsonHeader:
		req.add_header('Content-Type', 'application/json')
	if headers:
		for key, value in headers.items():
			req.add_header(key, value)

	resp = request.urlopen(req)
	if not decodeAs:
		return resp
	bytes = resp.read()
	if decodeAs == "bytes":
		return bytes
	if decodeAs == "text":
		return bytes.decode("utf-8")
	if decodeAs == "json":
		return json.loads(bytes)
	raise Exception("Unsupported decode option:"+decodeAs)

This is all quite straightforward, with the exception of paging. Paging is way to deal with very large sets. It will break down the list into pages, we can “page” through over the api.

Lets decode whats going on here. After each query, shopify will give us the exact url to use for the next page of results. (It also gives us the url to the previous page…) The url is encoded in a http response header, so we need to extract it. And then loop until there is no more next page.

nextLink = resp.info().get("link") reads the HTTP response header. The header might contain a only a next link (for the very first page), only a prev-link (for the last page) or both next and prev links. Thus we have to do a bit of parsing to find what we need.

Updating products quantity on Shopify

Updating the quantity of a product, we need the variant, warehouse and amount. This information is stored in Shopify as a InventoryItem. If we want to adjust the stock quantity for a product we have to find the right inventoryItem and issue a change with the set inventory level api.

Odoo Mutlichannel plugin

This is a rather useful plugin for warehouse integration. It offers to import products (and variants) to Shopify. It will also monitor Odoo stock changes and update shopify quantity for us.

Unfortunately the monitor is not reliable, due to technical and mostly user reasons (there are many ways products quantity can change that the plugin won't catch, or it will take too long and will be stopped).

Enter our updater, as a odoo scheduled action.

# Available variables:
#  - env: Odoo Environment on which the action is triggered
#  - model: Odoo Model of the record on which the action is triggered; is a void recordset
#  - record: record on which the action is triggered; may be void
#  - records: recordset of all records on which the action is triggered in multi-mode; may be void
#  - time, datetime, dateutil, timezone: useful Python libraries
#  - float_compare: Odoo function to compare floats based on specific precisions
#  - log: log(message, level='info'): logging function to record debug information in ir.logging table
#  - UserError: Warning Exception to use with raise
# To return an action, assign: action = {...}

initial_channel_ids = env['multi.channel.sale'].search([]).ids
channel_ids = initial_channel_ids #.copy()

count = 0
msg = ""

mappings = env['channel.product.mappings'].search([('channel_id', '=', 6)])

log("mapping_count: "+str(len(mappings)))

for mapping in mappings:
  
  # adjuct depending on your odoo cron timeout
  # this action will be scheduled again so we don't mind if don't catch all
  if count > 40:
    break

  channel_id = mapping.channel_id
  product_id = mapping.product_name
  qty = channel_id.get_quantity(product_id)
  # shopify_qty is a custom field on product, where we record what the last updated value was.
  # so we don't upload if not needed.
  if abs(qty - product_id.shopify_qty) < 0.1:
    continue
    
  count = count + 1
  msg = msg +"\n"+product_id.name + " qty:" + str(qty) + " shopify qty:"+str(product_id.shopify_qty)
  #_logger.info("update:" + channel_id.channel + " prod:" +product_id.name + " qty:" + str(qty))
  try:
    product_id.write({'shopify_qty': qty})
	# call the plugin method to call shopify
    channel_id.sync_quantity_shopify(mapping, qty)
  except Exception as e:
    msg = msg +"\n"+product_id.name + " ERROR " + str(e)
    pass

log("Updated :" + str(count)+msg)

Wrap

The technical hurdle with selling online is primarily integration. There are APIs for everything, but often it feels like putting a sqaure peg through a round hole.
And the landscape is continuosly evolving.

Whether you want to make your life easier with Shopify or you wish to do a custom integration, get in touch. We can help.

Happy shopping!

Continue reading

Manufacturing with Odoo

July 26, 2024
Manufacturing with Odoo

Do you run a manufacturing or assembling business?
Do you use Odoo already?
Do you plan to implement an ERP?
Let me share some insights and code for those running Manufacturing with Odoo.

App for counsellors and therapists

September 13, 2024
App for counsellors and therapists

Do you provide a counselling, wellness, fitness or therapy service?
Do you usually sell packages that consist of multiple visits?
We have an app for you!

React is easy (Csaba is dead)

September 06, 2024
React is easy (Csaba is dead)

99% of what you read and hear about react is dogmatic, religious almost fanatical garbage. React is a (html) rendering system, but more importantly it is an idea that with the powerful browser we have, we can simplify our dev lives.

©2022-2024 Inuko s.r.o
Privacy PolicyCloud
ENSKCZ
ICO 54507006
VAT SK2121695400
Vajanskeho 5
Senec 90301
Slovakia EU

contact@inuko.net