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, + }, +} +]]> + +