[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 <noreply@anthropic.com>
This commit is contained in:
parent
e625b0c2f3
commit
5766406b6f
4 changed files with 144 additions and 0 deletions
42
product_public_category_from_internal/README.md
Normal file
42
product_public_category_from_internal/README.md
Normal file
|
|
@ -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`).
|
||||
0
product_public_category_from_internal/__init__.py
Normal file
0
product_public_category_from_internal/__init__.py
Normal file
14
product_public_category_from_internal/__manifest__.py
Normal file
14
product_public_category_from_internal/__manifest__.py
Normal file
|
|
@ -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",
|
||||
],
|
||||
}
|
||||
88
product_public_category_from_internal/data/server_action.xml
Normal file
88
product_public_category_from_internal/data/server_action.xml
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="action_public_category_from_internal" model="ir.actions.server">
|
||||
<field name="name">Create eCommerce categories</field>
|
||||
<field name="model_id" ref="product.model_product_category" />
|
||||
<field name="binding_model_id" ref="product.model_product_category" />
|
||||
<field name="binding_view_types">list,form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code"><![CDATA[
|
||||
# Mirror the internal product categories (product.category) into the eCommerce
|
||||
# categories (product.public.category), keeping the same tree, and tag every
|
||||
# product template with the public category matching its internal one.
|
||||
# Run it from Inventory > 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,
|
||||
},
|
||||
}
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue