From 5766406b6f9f3d4a9af12e8ecebbb755dd0fa290 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Mon, 17 Aug 2026 19:42:01 +0200 Subject: [PATCH] [ADD] product_public_category_from_internal: mirror internal categories into eCommerce ones Contextual server action on product.category that reproduces the internal category tree as product.public.category records and tags every product template with the public category matching its own categ_id. Ancestors of the selected categories are pulled in so a branch is never orphaned at the root, and categories are walked in parent_path order so the public parent always exists before its children. Matching existing public categories by name + parent keeps the action idempotent. Co-Authored-By: Claude Opus 5 --- .../README.md | 42 +++++++++ .../__init__.py | 0 .../__manifest__.py | 14 +++ .../data/server_action.xml | 88 +++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 product_public_category_from_internal/README.md create mode 100644 product_public_category_from_internal/__init__.py create mode 100644 product_public_category_from_internal/__manifest__.py create mode 100644 product_public_category_from_internal/data/server_action.xml diff --git a/product_public_category_from_internal/README.md b/product_public_category_from_internal/README.md new file mode 100644 index 0000000..fe0e666 --- /dev/null +++ b/product_public_category_from_internal/README.md @@ -0,0 +1,42 @@ +# eCommerce Categories From Internal Categories + +Adds a contextual server action **Create eCommerce categories** to the Product +Categories (`product.category`) list and form views. + +It mirrors the internal category tree into the eCommerce categories +(`product.public.category`) and tags each product template with the public +category matching its internal one. + +## Usage + +1. Go to *Inventory → Configuration → Product Categories* (list view). +2. Select the categories you want, or select none to process them all. +3. *Actions → Create eCommerce categories*. + +For each internal category the action: + +- pulls in its ancestors, so the whole branch is reproduced; +- looks for an eCommerce category with the same name under the same + (already mirrored) parent, and creates it if it does not exist; +- adds that eCommerce category to every product template whose `categ_id` + is the internal one. + +A notification reports categories processed / created / reused and how many +products were tagged. + +## Notes + +- **Idempotent**: running it again matches the existing eCommerce categories by + name + parent and only tags the products that are still missing the link. +- Products keep any eCommerce category they already had (`Command.link`). To + make the internal category the *only* eCommerce category of the product, + swap that line in `data/server_action.xml` for `Command.set(public.ids)`. +- Only active products are tagged (`search` filters archived records out). +- The created categories have no `website_id`, so they are shared by every + website. Set it afterwards if you run a multi-website setup. +- The action is data, not code: once installed you can edit it live from + *Settings → Technical → Actions → Server Actions* without touching the addon. + +## Requirements + +Depends on `website_sale` (which defines `product.public.category`). diff --git a/product_public_category_from_internal/__init__.py b/product_public_category_from_internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/product_public_category_from_internal/__manifest__.py b/product_public_category_from_internal/__manifest__.py new file mode 100644 index 0000000..6474503 --- /dev/null +++ b/product_public_category_from_internal/__manifest__.py @@ -0,0 +1,14 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "eCommerce Categories From Internal Categories", + "summary": "Contextual action that mirrors the internal product categories " + "into the eCommerce categories and tags each product with its match.", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Criptomart", + "depends": ["website_sale"], + "data": [ + "data/server_action.xml", + ], +} diff --git a/product_public_category_from_internal/data/server_action.xml b/product_public_category_from_internal/data/server_action.xml new file mode 100644 index 0000000..e636a11 --- /dev/null +++ b/product_public_category_from_internal/data/server_action.xml @@ -0,0 +1,88 @@ + + + + Create eCommerce categories + + + list,form + code + Configuration > Product Categories: select the +# categories you want, or select none to process them all. +InternalCategory = env["product.category"] +PublicCategory = env["product.public.category"] +Template = env["product.template"] + +categories = records or record or InternalCategory.search([]) + +# Pull in the ancestors too, otherwise a selected child category would end up +# at the root of the eCommerce tree instead of under its own parent. +while True: + missing = categories.parent_id - categories + if not missing: + break + categories |= missing + +# parent_path ("1/5/9/") sorts every parent before its children, so the public +# parent always exists by the time we create its children. +categories = categories.sorted(lambda category: category.parent_path or "") + +public_by_internal = {} +created = 0 +reused = 0 + +for category in categories: + parent = public_by_internal.get(category.parent_id.id) + parent_id = parent.id if parent else False + public = PublicCategory.search( + [("name", "=", category.name), ("parent_id", "=", parent_id)], + limit=1, + ) + if public: + reused += 1 + else: + public = PublicCategory.create({ + "name": category.name, + "parent_id": parent_id, + }) + created += 1 + public_by_internal[category.id] = public + +tagged = 0 + +for internal_id, public in public_by_internal.items(): + templates = Template.search([ + ("categ_id", "=", internal_id), + ("public_categ_ids", "not in", public.ids), + ]) + if not templates: + continue + # Command.link adds the category and keeps the ones already set. Replace it + # with Command.set(public.ids) if you want the internal category to be the + # only eCommerce category of the product. + templates.write({"public_categ_ids": [Command.link(public.id)]}) + tagged += len(templates) + +message = ( + "Categories processed: %s\n" + "eCommerce categories created: %s\n" + "eCommerce categories already existing: %s\n" + "Products tagged: %s" +) % (len(categories), created, reused, tagged) + +action = { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": "eCommerce categories", + "message": message, + "type": "success" if created or tagged else "warning", + "sticky": False, + }, +} +]]> + +