diff --git a/shoppingfeed_integration/README.rst b/shoppingfeed_integration/README.rst new file mode 100644 index 0000000..76053ed --- /dev/null +++ b/shoppingfeed_integration/README.rst @@ -0,0 +1,256 @@ +======================== +Shoppingfeed Integration +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:900310fb90d1b308542f357095bbe83e185157a9bf74ec78154f8f636d4d8098 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fshoppingfeed-lightgray.png?logo=github + :target: https://github.com/OCA/shoppingfeed/tree/18.0/shoppingfeed_integration + :alt: OCA/shoppingfeed +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/shoppingfeed-18-0/shoppingfeed-18-0-shoppingfeed_integration + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/shoppingfeed&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module integrates Odoo with Shoppingfeed, a platform that connects +your store with multiple marketplaces. + +- Export product catalogs to Shoppingfeed in XML format +- Import orders from connected marketplaces automatically +- Synchronize product prices and stock levels in real-time +- Map marketplace channels to Odoo sale order types +- Map delivery carriers from marketplaces to Odoo carriers +- Track Shoppingfeed tickets and notifications +- Configure export filters by product type, category, and stock + availability + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Store Setup +----------- + +Create and authenticate your store +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Go to **Shoppingfeed → Stores** +2. Click **New** and fill: + + - **Username**: Shoppingfeed API email + - **Password**: Shoppingfeed API password + +3. Click **Get Token** button + + - Automatically retrieves: catalog ID, channels, country, and + language + +Configure product export +~~~~~~~~~~~~~~~~~~~~~~~~ + +In the store form, configure these tabs: + +Exportable Products tab +^^^^^^^^^^^^^^^^^^^^^^^ + +- **Export only selected**: Only export products with "Export to + Shoppingfeed" enabled +- **Export Product Types**: Select product types to include (Goods, + Services, Combos) +- **Export Rules**: Control which products to include: + + - Out of stock products + - Archived products + - Non-salable products + +Stock tab +^^^^^^^^^ + +- **Use actual stock state**: Use real quantities vs default quantity +- **Quantity type**: Salable (on-hand) or Virtual (forecasted) +- **Default quantity**: Quantity for products without stock tracking +- **Force zero quantity non salable**: Set 0 for non-salable products +- **Update quantities realtime**: Push stock changes immediately to + Shoppingfeed + +Prices tab +^^^^^^^^^^ + +- **Pricelist**: Optional pricelist for computing export prices + +Attributes tab +^^^^^^^^^^^^^^ + +- **Use product ID as SKU**: Use product ID instead of default code +- **Custom SKU field**: Select custom field for SKU (optional) +- **Additional attribute fields**: Add custom product fields to export + +Images tab +^^^^^^^^^^ + +- **Export all images**: Include all product images +- **Exported image count**: Limit number of images (if not exporting + all) + +Categories tab +^^^^^^^^^^^^^^ + +- **Allowed categories**: Restrict export to specific product categories + +Shipping tab +^^^^^^^^^^^^ + +- **Default delivery carrier**: Fallback carrier for unmapped carriers +- **Carrier mappings**: Map Shoppingfeed carrier names to Odoo carriers + + - Add lines with Shoppingfeed carrier name and corresponding Odoo + carrier + +Orders tab +^^^^^^^^^^ + +- **Import orders**: Enable automatic order import +- **Default payment term**: Payment terms for imported orders +- **Default payment mode**: Payment mode for imported orders +- **Default order type**: Order type for imported orders +- **Marketplace customer groups**: Map channels to specific order types + + - Add lines with Channel and Order Type + +Product Setup +------------- + +Mark products for export +~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Open a product +2. In **Shoppingfeed** tab: + + - Enable **Export to Shoppingfeed** (if store requires it) + - Select **Shoppingfeed Stores** to export this product to + +**Note**: Products need default code (internal reference) to export. + +Catalog Feed +------------ + +After configuration, your XML catalog feed is available at: + +:: + + https://yourdomain.com/catalog/{catalog_id}.xml + +Share this URL with Shoppingfeed for product synchronization. + +Usage +===== + +Product Updates +--------------- + +Price changes +~~~~~~~~~~~~~ + +Product price updates send automatically to linked Shoppingfeed stores. + +Stock changes +~~~~~~~~~~~~~ + +Stock updates send automatically if store has **Update quantities +realtime** enabled. + +Orders +------ + +View orders +~~~~~~~~~~~ + +Go to **Shoppingfeed → Marketplace Orders** to see marketplace orders. + +Orders show: + +- Shoppingfeed Order ID +- Shoppingfeed Reference +- Shoppingfeed Store +- Shoppingfeed Channel (Amazon, eBay, etc.) +- Shoppingfeed Status + +Process orders +~~~~~~~~~~~~~~ + +1. Confirm order +2. Validate delivery → shipment info sends to Shoppingfeed +3. Validate invoice → PDF uploads to Shoppingfeed (if channel supports + it) + +Orders auto-acknowledge in Shoppingfeed on import. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Juan Carlos Oñate + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-juancarlosonate-tecnativa| image:: https://github.com/juancarlosonate-tecnativa.png?size=40px + :target: https://github.com/juancarlosonate-tecnativa + :alt: juancarlosonate-tecnativa + +Current `maintainer `__: + +|maintainer-juancarlosonate-tecnativa| + +This module is part of the `OCA/shoppingfeed `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/shoppingfeed_integration/__init__.py b/shoppingfeed_integration/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/shoppingfeed_integration/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/shoppingfeed_integration/__manifest__.py b/shoppingfeed_integration/__manifest__.py new file mode 100644 index 0000000..4e063d9 --- /dev/null +++ b/shoppingfeed_integration/__manifest__.py @@ -0,0 +1,36 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Shoppingfeed Integration", + "version": "18.0.1.0.4", + "summary": "Integrate Odoo with Shoppingfeed for product export and order sync", + "category": "Sales", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["juancarlosonate-tecnativa"], + "website": "https://github.com/OCA/shoppingfeed", + "license": "AGPL-3", + "depends": [ + "stock", + "website_sale", + "product_brand", + "sale_order_type", + "account_payment_sale", + "phone_validation", + ], + "data": [ + "security/shoppingfeed_security.xml", + "security/ir.model.access.csv", + "security/ir_rule.xml", + "views/sale_order_views.xml", + "views/shoppingfeed_ticket_views.xml", + "views/shoppingfeed_store_views.xml", + "views/shoppingfeed_log_views.xml", + "views/product_template_views.xml", + "views/product_pricelist_views.xml", + "views/shoppingfeed_channel_views.xml", + "views/product_attribute_views.xml", + "views/menus.xml", + "data/cron.xml", + ], + "installable": True, +} diff --git a/shoppingfeed_integration/controllers/__init__.py b/shoppingfeed_integration/controllers/__init__.py new file mode 100644 index 0000000..e67fc18 --- /dev/null +++ b/shoppingfeed_integration/controllers/__init__.py @@ -0,0 +1 @@ +from . import catalog diff --git a/shoppingfeed_integration/controllers/catalog.py b/shoppingfeed_integration/controllers/catalog.py new file mode 100644 index 0000000..7cbbf00 --- /dev/null +++ b/shoppingfeed_integration/controllers/catalog.py @@ -0,0 +1,764 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import tempfile +from datetime import datetime + +from lxml import etree + +from odoo import http, release +from odoo.http import Response, request + + +class CatalogController(http.Controller): + _PRODUCT_DATA_FIELDS = [ + "barcode", + "default_code", + "description_sale", + "name", + "product_brand_id", + "product_template_attribute_value_ids", + "product_template_image_ids", + "product_tmpl_id", + "public_categ_ids", + "sale_ok", + "sf_forced_category_id", + "taxes_id", + "type", + "website_description", + "website_url", + "weight", + ] + + _PRODUCT_FETCH_FIELDS = [ + *_PRODUCT_DATA_FIELDS, + "lst_price", + ] + + @http.route( + ["/catalog.xml", "/catalog/.xml"], + type="http", + auth="public", + website=True, + csrf=False, + ) + def catalog_feed(self, catalog_id=None, **kwargs): + # Generate Shoppingfeed product catalog XML for a specific store. + if not catalog_id: + return Response( + "Missing required catalog_id.", + status=400, + content_type="application/xml;charset=utf-8", + ) + store = ( + request.env["shoppingfeed.store"] + .sudo() + .search([("catalog_id", "=", catalog_id)], limit=1) + ) + if not store: + return Response( + f"Store with catalog_id {catalog_id} not found.", + status=404, + content_type="application/xml;charset=utf-8", + ) + catalog_file = self._generate_catalog_file(store) + return Response( + catalog_file, + content_type="application/xml;charset=utf-8", + status=200, + direct_passthrough=True, + ) + + def _get_product_domain_for_store(self, store): + domain = [] + if store.export_only_selected: + domain.append(("export_to_shoppingfeed", "=", True)) + domain.append(("shoppingfeed_store_ids", "in", store.id)) + # Product type filters + product_types = [] + if store.export_type_goods: + product_types.append("consu") + if store.export_type_service: + product_types.append("service") + if store.export_type_combo: + product_types.append("combo") + domain.append(("type", "in", product_types)) + if not store.export_out_of_stock: + domain.append(("qty_available", ">", 0)) + if not store.export_disabled_products: + domain.append(("active", "=", True)) + if not store.export_not_salable_products: + domain.append(("sale_ok", "=", True)) + if store.allowed_categ_ids: + domain.append(("public_categ_ids", "in", store.allowed_categ_ids.ids)) + return domain + + def _get_product_env_for_store(self, store): + env = request.env["product.product"].sudo().with_company(store.company_id) + if store.lang_id: + env = env.with_context(lang=store.lang_id.code) + if store.export_disabled_products: + env = env.with_context(active_test=False) + return env + + def _get_products_for_store(self, store): + return self._get_product_env_for_store(store).search( + self._get_product_domain_for_store(store) + ) + + def _generate_catalog_file(self, store): + catalog_file = tempfile.TemporaryFile() + for chunk in self._iter_catalog_xml(store): + catalog_file.write(chunk) + catalog_file.seek(0) + return catalog_file + + def _iter_catalog_xml(self, store, batch_size=500): + product_env = self._get_product_env_for_store(store) + domain = self._get_product_domain_for_store(store) + product_ids = product_env.search(domain).ids + total_products = len(product_ids) + generation_started_at = datetime.now() + catalog_context = self._get_catalog_context(store) + + yield b'\n' + yield b'\n\n' + for index in range(0, total_products, batch_size): + batch = product_env.browse(product_ids[index : index + batch_size]) + self._fetch_product_batch_fields(store, batch) + batch_context = self._get_product_batch_context(store, batch) + for product in batch: + product_el = self._build_product_xml( + store, product, catalog_context, batch_context + ) + yield etree.tostring(product_el, encoding="UTF-8") + yield b"\n" + request.env.invalidate_all() + yield b"\n" + metadata_el = self._build_metadata_xml(total_products, generation_started_at) + yield etree.tostring(metadata_el, encoding="UTF-8") + yield b"\n\n" + + def _get_catalog_context(self, store): + return { + "allowed_categ_ids": set(store.allowed_categ_ids.ids), + "base_url": store.get_base_url().rstrip("/"), + } + + def _get_product_field_names(self, store, base_fields): + field_names = list(base_fields) + if store.custom_sku_field_id: + field_names.append(store.custom_sku_field_id.name) + field_names.extend( + attr_field.field_id.name + for attr_field in store.additional_attribute_field_ids + ) + return list(dict.fromkeys(field_names)) + + def _fetch_product_batch_fields(self, store, products): + products.fetch(self._get_product_field_names(store, self._PRODUCT_FETCH_FIELDS)) + + def _get_product_batch_context(self, store, products): + product_data = self._get_product_data(store, products) + brand_names = self._get_brand_names(products, product_data) + category_names = self._get_category_names(store, products, product_data) + variant_attributes = self._get_variant_attributes(products, product_data) + no_variant_attributes = self._get_no_variant_attributes(products, product_data) + base_prices = self._get_base_prices(store, products) + quantity_values = self._get_quantity_values(store, products) + main_images = self._get_main_images(products, product_data) + taxes_by_key = self._get_taxes_by_key(store, products, product_data) + additional_prices = {} + for pricelist in store.additional_pricelist_ids: + if not pricelist.shoppingfeed_attribute_name: + continue + prices = self._get_pricelist_prices(store, pricelist, products) + additional_prices[pricelist.id] = { + product_id: pricelist.currency_id.round(price) + for product_id, price in prices.items() + } + return { + "additional_prices": additional_prices, + "base_prices": base_prices, + "brand_names": brand_names, + "category_names": category_names, + "main_images": main_images, + "no_variant_attributes": no_variant_attributes, + "product_data": product_data, + "quantity_values": quantity_values, + "taxes_by_key": taxes_by_key, + "variant_attributes": variant_attributes, + } + + def _get_product_data(self, store, products): + return { + data["id"]: data + for data in products.read( + self._get_product_field_names(store, self._PRODUCT_DATA_FIELDS) + ) + } + + def _get_brand_names(self, products, product_data): + brand_ids = { + data["product_brand_id"][0] + for data in product_data.values() + if data["product_brand_id"] + } + if not brand_ids: + return {} + return { + data["id"]: data["name"] + for data in products.env["product.brand"].browse(brand_ids).read(["name"]) + } + + def _get_category_names(self, store, products, product_data): + allowed_categ_ids = set(store.allowed_categ_ids.ids) + category_ids = { + category_id + for data in product_data.values() + for category_id in ( + [data["sf_forced_category_id"][0]] + if data["sf_forced_category_id"] + else [ + public_categ_id + for public_categ_id in data["public_categ_ids"] + if public_categ_id in allowed_categ_ids + ] + ) + } + if not category_ids: + return {} + return { + data["id"]: data["display_name"].replace(" / ", " > ") + for data in products.env["product.public.category"] + .browse(category_ids) + .read(["display_name"]) + } + + def _get_variant_attributes(self, products, product_data): + product_ptav_ids = { + product_id: data["product_template_attribute_value_ids"] + for product_id, data in product_data.items() + } + ptav_ids = { + ptav_id for value_ids in product_ptav_ids.values() for ptav_id in value_ids + } + if not ptav_ids: + return {} + ptav_data = ( + products.env["product.template.attribute.value"] + .browse(ptav_ids) + .read(["attribute_id", "name"]) + ) + attribute_ids = { + data["attribute_id"][0] for data in ptav_data if data["attribute_id"] + } + attribute_data = self._get_attribute_data(products, attribute_ids) + ptav_map = { + data["id"]: { + "attribute": attribute_data.get(data["attribute_id"][0]), + "value": data["name"], + } + for data in ptav_data + if data["attribute_id"] + } + return { + product_id: [ + { + "name": ptav_map[ptav_id]["attribute"]["export_name"], + "value": ptav_map[ptav_id]["value"], + } + for ptav_id in value_ids + if ptav_id in ptav_map + and ptav_map[ptav_id]["attribute"] + and ptav_map[ptav_id]["attribute"]["shoppingfeed_export"] + ] + for product_id, value_ids in product_ptav_ids.items() + } + + def _get_no_variant_attributes(self, products, product_data): + template_ids = { + data["product_tmpl_id"][0] + for data in product_data.values() + if data["product_tmpl_id"] + } + if not template_ids: + return {} + lines_data = products.env["product.template.attribute.line"].search_read( + [("product_tmpl_id", "in", list(template_ids))], + ["attribute_id", "product_tmpl_id", "value_ids"], + order="sequence, attribute_id, id", + ) + attribute_ids = { + data["attribute_id"][0] for data in lines_data if data["attribute_id"] + } + attribute_data = self._get_attribute_data(products, attribute_ids) + value_ids = { + value_id for data in lines_data for value_id in data.get("value_ids", []) + } + value_names = {} + if value_ids: + value_names = { + data["id"]: data["name"] + for data in products.env["product.attribute.value"] + .browse(value_ids) + .read(["name"]) + } + result = {} + for data in lines_data: + attribute_id = data["attribute_id"] and data["attribute_id"][0] + attribute = attribute_data.get(attribute_id) + if ( + not attribute + or attribute["create_variant"] != "no_variant" + or not attribute["shoppingfeed_export"] + or not data["value_ids"] + ): + continue + template_id = data["product_tmpl_id"][0] + result.setdefault(template_id, []).append( + { + "name": attribute["export_name"], + "value": ", ".join( + value_names[value_id] + for value_id in data["value_ids"] + if value_id in value_names + ), + } + ) + return result + + def _get_attribute_data(self, products, attribute_ids): + if not attribute_ids: + return {} + return { + data["id"]: { + "create_variant": data["create_variant"], + "export_name": data["shoppingfeed_code_name_attribute"] or data["name"], + "shoppingfeed_export": data["shoppingfeed_export"], + } + for data in products.env["product.attribute"] + .browse(attribute_ids) + .read( + [ + "create_variant", + "name", + "shoppingfeed_code_name_attribute", + "shoppingfeed_export", + ] + ) + } + + def _get_base_prices(self, store, products): + currency = ( + store.pricelist_id.currency_id + if store.pricelist_id + else store.company_id.currency_id + ) + if store.pricelist_id: + prices = self._get_pricelist_prices(store, store.pricelist_id, products) + else: + prices = {product.id: product.lst_price or 0.0 for product in products} + return { + product_id: currency.round(price) for product_id, price in prices.items() + } + + def _get_pricelist_prices(self, store, pricelist, products): + return pricelist._get_products_price(products, 1.0, None) + + def _get_taxes_by_key(self, store, products, product_data): + if not store.include_taxes_in_price: + return {} + tax_keys = { + tuple(data["taxes_id"]) + for data in product_data.values() + if data["taxes_id"] + } + return { + tax_key: products.env["account.tax"] + .browse(tax_key) + ._filter_taxes_by_company(store.company_id) + for tax_key in tax_keys + } + + def _get_quantity_values(self, store, products): + if not store.use_actual_stock_state: + return {} + if store.quantity_type == "virtual": + return { + data["id"]: data["virtual_available"] + for data in products.read(["virtual_available"]) + } + return dict(products._get_only_qty_available()) + + def _get_main_images(self, products, product_data=None): + if product_data is None: + return self._get_main_images_from_binary_fields(products) + product_ids = list(product_data) + template_ids = { + data["product_tmpl_id"][0] + for data in product_data.values() + if data["product_tmpl_id"] + } + attachments = products.env["ir.attachment"].sudo() + product_image_ids = set( + attachments.search( + [ + ("res_model", "=", "product.product"), + ("res_field", "=", "image_variant_1920"), + ("res_id", "in", product_ids), + ] + ).mapped("res_id") + ) + template_image_ids = set( + attachments.search( + [ + ("res_model", "=", "product.template"), + ("res_field", "=", "image_1920"), + ("res_id", "in", list(template_ids)), + ] + ).mapped("res_id") + ) + return { + product_id: product_id in product_image_ids + or ( + data["product_tmpl_id"] + and data["product_tmpl_id"][0] in template_image_ids + ) + for product_id, data in product_data.items() + } + + def _get_main_images_from_binary_fields(self, products): + products_data = products.with_context(bin_size=True).read( + ["image_variant_1920", "product_tmpl_id"] + ) + template_ids = { + data["product_tmpl_id"][0] + for data in products_data + if data["product_tmpl_id"] and not data["image_variant_1920"] + } + template_images = { + data["id"]: bool(data["image_1920"]) + for data in products.env["product.template"] + .browse(template_ids) + .with_context(bin_size=True) + .read(["image_1920"]) + } + return { + data["id"]: bool(data["image_variant_1920"]) + or template_images.get(data["product_tmpl_id"][0], False) + for data in products_data + } + + def _build_catalog_xml(self, store, products, base_url): + catalog_el = etree.Element("catalog") + products_el = etree.SubElement(catalog_el, "products", version="1.0.0") + catalog_context = self._get_catalog_context(store) + if base_url: + catalog_context["base_url"] = base_url.rstrip("/") + batch_context = self._get_product_batch_context(store, products) + for product in products: + products_el.append( + self._build_product_xml(store, product, catalog_context, batch_context) + ) + self._add_metadata(catalog_el, len(products)) + return catalog_el + + def _build_product_xml(self, store, product, catalog_context, batch_context): + product_el = etree.Element("product") + self._add_product_base_info( + store, product_el, product, catalog_context, batch_context + ) + self._add_product_price(store, product_el, product, batch_context) + self._add_product_stock(store, product_el, product, batch_context) + self._add_product_media( + store, product_el, product, catalog_context, batch_context + ) + self._add_product_attributes(store, product_el, product, batch_context) + return product_el + + def _get_base_price(self, store, product, batch_context=None): + if batch_context: + price = batch_context["base_prices"].get(product.id, 0.0) + else: + price = product.lst_price or 0.0 + if store.pricelist_id: + price = store.pricelist_id._get_product_price(product, 1.0, None) + currency = ( + store.pricelist_id.currency_id + if store.pricelist_id + else store.company_id.currency_id + ) + return currency.round(price) + + def _get_batch_product_data(self, product, batch_context=None): + if not batch_context: + return {} + return batch_context["product_data"].get(product.id, {}) + + def _add_product_base_info( + self, store, product_el, product, catalog_context, batch_context + ): + product_data = batch_context["product_data"][product.id] + base_url = catalog_context["base_url"] + if store.use_product_id_as_sku: + sku_value = str(product.id) + elif store.custom_sku_field_id: + sku_value = product_data.get(store.custom_sku_field_id.name) or str( + product.id + ) + else: + sku_value = product_data["default_code"] or str(product.id) + etree.SubElement( + product_el, "reference" + ).text = f"{sku_value}_{store.country_id.code}" + etree.SubElement(product_el, "gtin").text = product_data["barcode"] or "" + etree.SubElement(product_el, "name").text = etree.CDATA( + product_data["name"] or "" + ) + if product_data["website_url"]: + product_url = base_url + product_data["website_url"] + etree.SubElement(product_el, "link").text = etree.CDATA(product_url) + if product_data["weight"]: + etree.SubElement(product_el, "weight").text = str(product_data["weight"]) + if product_data["product_brand_id"]: + brand_el = etree.SubElement(product_el, "brand") + etree.SubElement(brand_el, "name").text = etree.CDATA( + batch_context["brand_names"].get( + product_data["product_brand_id"][0], "" + ) + ) + if product_data["sf_forced_category_id"]: + category_id = product_data["sf_forced_category_id"][0] + else: + category_id = next( + ( + category_id + for category_id in product_data["public_categ_ids"] + if category_id in catalog_context["allowed_categ_ids"] + ), + False, + ) + if category_id: + category_el = etree.SubElement(product_el, "category") + etree.SubElement(category_el, "name").text = etree.CDATA( + batch_context["category_names"].get(category_id, "") + ) + etree.SubElement(category_el, "link").text = etree.CDATA( + f"{base_url}/shop/category/{category_id}" + ) + description_el = etree.SubElement(product_el, "description") + full_desc = product_data["website_description"] or "" + etree.SubElement(description_el, "full").text = etree.CDATA(full_desc) + short_desc = product_data["description_sale"] or "" + etree.SubElement(description_el, "short").text = etree.CDATA(short_desc) + + def _add_product_price(self, store, product_el, product, batch_context=None): + product_data = self._get_batch_product_data(product, batch_context) + price = self._get_base_price(store, product, batch_context) + tax_ids = product_data.get("taxes_id") if product_data else product.taxes_id.ids + if store.include_taxes_in_price and tax_ids: + currency = ( + store.pricelist_id.currency_id + if store.pricelist_id + else store.company_id.currency_id + ) + if batch_context: + product_taxes = batch_context["taxes_by_key"][tuple(tax_ids)] + else: + product_taxes = product.env["account.tax"].browse(tax_ids) + product_taxes = product_taxes._filter_taxes_by_company(store.company_id) + tax_result = product_taxes.compute_all( + price, currency=currency, quantity=1.0, product=product + ) + price = currency.round(tax_result["total_included"]) + etree.SubElement(product_el, "price").text = str(price) + + def _get_quantity_value(self, store, product, batch_context=None): + if batch_context: + return batch_context["quantity_values"].get(product.id, 0.0) + if store.quantity_type == "virtual": + return product.virtual_available + return product.qty_available + + def _add_product_stock(self, store, product_el, product, batch_context=None): + product_data = self._get_batch_product_data(product, batch_context) + if not store.use_actual_stock_state: + quantity_value = store.default_quantity + else: + quantity_value = ( + self._get_quantity_value(store, product, batch_context) or 0 + ) + product_type = product_data.get("type") if product_data else product.type + if product_type == "service": + quantity_value = store.default_quantity + sale_ok = product_data.get("sale_ok") if product_data else product.sale_ok + if not sale_ok and store.force_zero_quantity_non_salable: + quantity_value = 0 + etree.SubElement(product_el, "quantity").text = str(int(quantity_value)) + + def _add_product_media( + self, store, product_el, product, catalog_context, batch_context=None + ): + product_data = self._get_batch_product_data(product, batch_context) + base_url = catalog_context["base_url"] + images_el = etree.SubElement(product_el, "images") + product_images = ( + product_data.get("product_template_image_ids") + if product_data + else product.product_template_image_ids.ids + ) + if store.export_all_images: + all_images = product_images + else: + all_images = product_images[: store.exported_image_count] + if self._has_main_image(product, batch_context): + etree.SubElement(images_el, "image", type="main").text = etree.CDATA( + f"{base_url}/web/image/product.product/{product.id}/image_1920" + ) + for image_id in all_images: + etree.SubElement(images_el, "image").text = etree.CDATA( + f"{base_url}/web/image/product.image/{image_id}/image_1920" + ) + + def _has_main_image(self, product, batch_context=None): + if batch_context: + return batch_context["main_images"].get(product.id, False) + return self._get_main_images(product).get(product.id, False) + + def _add_product_attributes( # noqa: C901 + self, store, product_el, product, batch_context=None + ): + product_data = self._get_batch_product_data(product, batch_context) + attributes_el = etree.SubElement(product_el, "attributes") + # Attributes that generate variants + if batch_context: + variant_attributes = batch_context["variant_attributes"].get(product.id, []) + else: + variant_attributes = [] + for value in product.product_template_attribute_value_ids: + if not value.attribute_id.shoppingfeed_export: + continue + variant_attributes.append( + { + "name": value.attribute_id.shoppingfeed_code_name_attribute + or value.attribute_id.name, + "value": value.name, + } + ) + for attribute in variant_attributes: + attr_el = etree.SubElement(attributes_el, "attribute") + etree.SubElement(attr_el, "name").text = attribute["name"] + etree.SubElement(attr_el, "value").text = attribute["value"] + # Attributes that do NOT generate variants + if batch_context: + template_id = product_data["product_tmpl_id"][0] + no_variant_attributes = batch_context["no_variant_attributes"].get( + template_id, [] + ) + else: + no_variant_attributes = [] + for line in product.product_tmpl_id.attribute_line_ids.filtered( + lambda line_var: ( + line_var.attribute_id.create_variant == "no_variant" + and line_var.attribute_id.shoppingfeed_export + ) + ): + if line.value_ids: + no_variant_attributes.append( + { + "name": line.attribute_id.shoppingfeed_code_name_attribute + or line.attribute_id.name, + "value": ", ".join(line.value_ids.mapped("name")), + } + ) + for attribute in no_variant_attributes: + attr_el = etree.SubElement(attributes_el, "attribute") + etree.SubElement(attr_el, "name").text = attribute["name"] + etree.SubElement(attr_el, "value").text = attribute["value"] + if store.additional_attribute_field_ids: + for attr_field in store.additional_attribute_field_ids: + field = attr_field.field_id + value = ( + product_data.get(field.name) + if product_data + else getattr(product, field.name, False) + ) + if not value: + continue + attr_el = etree.SubElement(attributes_el, "attribute") + attr_name = ( + attr_field.custom_name or field.field_description or field.name + ) + etree.SubElement(attr_el, "name").text = attr_name + if field.ttype == "many2one": + if isinstance(value, tuple | list): + attr_value = value[1] if len(value) > 1 else str(value[0]) + else: + attr_value = ( + value.display_name + if hasattr(value, "display_name") + else str(value.id) + ) + etree.SubElement(attr_el, "value").text = attr_value + elif field.ttype == "boolean": + etree.SubElement(attr_el, "value").text = ( + "True" if value else "False" + ) + elif field.ttype == "html": + etree.SubElement(attr_el, "value").text = etree.CDATA(str(value)) + elif field.ttype == "datetime": + etree.SubElement(attr_el, "value").text = etree.CDATA(str(value)) + else: + etree.SubElement(attr_el, "value").text = str(value) + self._add_product_price_attributes(store, attributes_el, product, batch_context) + + def _add_product_price_attributes( + self, store, attributes_el, product, batch_context=None + ): + # Export price without taxes as additional attribute for marketplaces + if store.export_price_without_tax and store.price_without_tax_attribute_name: + price_without_tax = self._get_base_price(store, product, batch_context) + attr_el = etree.SubElement(attributes_el, "attribute") + etree.SubElement( + attr_el, "name" + ).text = store.price_without_tax_attribute_name + etree.SubElement(attr_el, "value").text = str(price_without_tax) + # Additional pricelists as price attributes + if store.additional_pricelist_ids: + for pricelist in store.additional_pricelist_ids: + if not pricelist.shoppingfeed_attribute_name: + continue + if batch_context: + price = batch_context["additional_prices"][pricelist.id].get( + product.id, 0.0 + ) + else: + price = pricelist.currency_id.round( + pricelist._get_product_price(product, 1.0, None) + ) + attr_el = etree.SubElement(attributes_el, "attribute") + etree.SubElement( + attr_el, "name" + ).text = pricelist.shoppingfeed_attribute_name + etree.SubElement(attr_el, "value").text = str(price) + + def _build_metadata_xml(self, total_products, started_at): + metadata_el = etree.Element("metadata") + self._add_metadata_values(metadata_el, total_products, started_at) + return metadata_el + + def _add_metadata(self, catalog_el, total_products): + metadata_el = etree.SubElement(catalog_el, "metadata") + self._add_metadata_values(metadata_el, total_products, datetime.now()) + + def _add_metadata_values(self, metadata_el, total_products, started_at): + etree.SubElement(metadata_el, "platform").text = f"Odoo:{release.version}" + etree.SubElement( + metadata_el, "agent" + ).text = f"shoppingfeed_integration:{release.version}" + etree.SubElement(metadata_el, "startedAt").text = started_at.isoformat() + etree.SubElement(metadata_el, "finishedAt").text = datetime.now().isoformat() + etree.SubElement(metadata_el, "invalid").text = "0" + etree.SubElement(metadata_el, "ignored").text = "0" + etree.SubElement(metadata_el, "written").text = str(total_products) diff --git a/shoppingfeed_integration/data/catalog_raw.xml b/shoppingfeed_integration/data/catalog_raw.xml new file mode 100644 index 0000000..5964a47 --- /dev/null +++ b/shoppingfeed_integration/data/catalog_raw.xml @@ -0,0 +1,302 @@ + + + + + + AB123 + + 123456789> + + + + + + 10 + + 1 + + 19.0 + + 0.56 + + 2.5 + + + + + + + + Home > Mugs ]]> + + + + + 8 + + + + + + + + + + 5.90 + + + + + + + + + + + + + Capacity + 10 + + + Material + Iron + + + + + + AB123-1 + 123456789> + 1 + 10 + + + 2.5 + + 10 + + + + 5.90 + + + + + + + + + + + + Color + Red + + + Size + Small + + + + + + AB123-1 + 123456789> + 1 + 10 + + 10 + + + + 5.90 + + + + + + + + + + + Color + Blue + + + Size + Medium + + + + + + + + AB123 + 123456789> + + + 10 + 1 + + + + + + Home > Mugs ]]> + + + + 10 + + + + + + + + 5.90 + + + + + + + + + + + Color + Red + + + Size + XML + + + Capacity + 10 + + + Material + Iron + + + + + AB123 + 123456789> + 1 + 10 + + 10 + + + + 5.90 + + + + + + + + + + + Color + Red + + + Capacity + 10 + + + + + AB123 + 123456789> + 1 + 10 + + 10 + + + + 5.90 + + + + + + + + + + + Color + Red + + + Capacity + 10 + + + + + + + + shopping-feed-php-sdk:1.0.0 + Magento:2.0.1 + 2018-05-30T09:00:00 + 2018-05-30T09:20:00 + 1 + 0 + 10 + + diff --git a/shoppingfeed_integration/data/cron.xml b/shoppingfeed_integration/data/cron.xml new file mode 100644 index 0000000..7e8d619 --- /dev/null +++ b/shoppingfeed_integration/data/cron.xml @@ -0,0 +1,21 @@ + + + + Shoppingfeed: Import Orders + + code + model.action_import_from_shoppingfeed() + 15 + minutes + False + + + Shoppingfeed: Import Tickets + + code + model.cron_fetch_all_tickets() + 60 + minutes + True + + diff --git a/shoppingfeed_integration/data/neutralize.sql b/shoppingfeed_integration/data/neutralize.sql new file mode 100644 index 0000000..be0a2d8 --- /dev/null +++ b/shoppingfeed_integration/data/neutralize.sql @@ -0,0 +1,3 @@ +UPDATE shoppingfeed_store + SET demo_mode = TRUE + WHERE demo_mode IS DISTINCT FROM TRUE; diff --git a/shoppingfeed_integration/i18n/es.po b/shoppingfeed_integration/i18n/es.po new file mode 100644 index 0000000..5655770 --- /dev/null +++ b/shoppingfeed_integration/i18n/es.po @@ -0,0 +1,1148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shoppingfeed_integration +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-01-22 11:45+0000\n" +"PO-Revision-Date: 2026-01-22 11:45+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__access_token +msgid "Access Token" +msgstr "Token de Acceso" + +#. module: shoppingfeed_integration +#: model:res.groups,comment:shoppingfeed_integration.group_shoppingfeed_user +msgid "Access to view Shoppingfeed stores, channels, and tickets." +msgstr "Acceso para ver tiendas, canales y tickets de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__email +msgid "Account Email" +msgstr "Correo Electrónico de la Cuenta" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Account Information" +msgstr "Información de la Cuenta" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_needaction +msgid "Action Needed" +msgstr "Acción Necesaria" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__additional_attribute_field_ids +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Additional Attributes" +msgstr "Atributos Adicionales" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_attachment_count +msgid "Attachment Count" +msgstr "Cantidad de Adjuntos" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Attributes" +msgstr "Atributos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__canceled +msgid "Canceled" +msgstr "Cancelado" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__carrier_map_ids +msgid "Carrier Mappings" +msgstr "Asignaciones de Transportistas" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__carrier_name +msgid "Carrier name as received from Shoppingfeed" +msgstr "Nombre del transportista tal como se recibe desde Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__feed_url +msgid "Catalog Feed URL" +msgstr "URL del Feed del Catálogo" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Categories" +msgstr "Categorías" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__allowed_categ_ids +msgid "Category Selection" +msgstr "Selección de Categorías" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__channel_id +msgid "Channel" +msgstr "Canal" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_channel +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__channel_ids +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_channel +msgid "Channels" +msgstr "Canales" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__quantity_type +msgid "" +"Choose which stock quantity to export: 'Salable' uses qty_available, " +"'Virtual' uses virtual_available." +msgstr "" +"Elija qué cantidad de stock exportar: 'Vendible' usa qty_available, " +"'Virtual' usa virtual_available." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_type_combo +msgid "Combos" +msgstr "Combos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__company_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__company_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__company_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__company_id +msgid "Company" +msgstr "Compañía" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_raw_data +msgid "Complete JSON data received from Shoppingfeed API for this order." +msgstr "Datos JSON completos recibidos de la API de Shoppingfeed para este pedido." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__country_id +msgid "Country" +msgstr "País" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Created On" +msgstr "Creado el" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Credentials" +msgstr "Credenciales" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_status +msgid "Current status of the order in Shoppingfeed." +msgstr "Estado actual del pedido en Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__custom_name +msgid "Custom Attribute Name" +msgstr "Nombre de Atributo Personalizado" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__custom_sku_field_id +msgid "Custom SKU Attribute" +msgstr "Atributo SKU Personalizado" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_product_attribute__shoppingfeed_code_name_attribute +msgid "Custom attribute name used when exporting products to Shoppingfeed." +msgstr "Nombre de atributo personalizado usado al exportar productos a Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__custom_name +msgid "" +"Custom name to use in the exported catalog. If empty, the field description " +"will be used." +msgstr "Nombre personalizado para usar en el catálogo exportado. Si está vacío, se usará la descripción del campo." + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_log_list +msgid "Date" +msgstr "Fecha" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_delivery_carrier_id +msgid "Default Delivery Carrier" +msgstr "Transportista de Entrega por Defecto" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_order_type_id +msgid "Default Order Type" +msgstr "Tipo de Pedido por Defecto" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_payment_mode_id +msgid "Default Payment Mode" +msgstr "Modo de Pago por Defecto" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_payment_term_id +msgid "Default Payment Terms" +msgstr "Términos de Pago por Defecto" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_quantity +msgid "Default Quantity" +msgstr "Cantidad por Defecto" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_order_type_id +msgid "" +"Default Sale Order Type to assign to imported orders when no other type is " +"detected or explicitly set from the Shoppingfeed source." +msgstr "" +"Tipo de Pedido de Venta por Defecto para asignar a pedidos importados cuando" +" no se detecta otro tipo o no se establece explícitamente desde " +"Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_delivery_carrier_id +msgid "" +"Default carrier to assign to imported orders when the carrier from " +"Shoppingfeed is not mapped." +msgstr "" +"Transportista por defecto para asignar a pedidos importados cuando el " +"transportista de Shoppingfeed no está mapeado." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_payment_mode_id +msgid "Default payment mode to assign to orders when no other is provided." +msgstr "" +"Modo de pago por defecto para asignar a pedidos cuando no se proporciona " +"otro." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_payment_term_id +msgid "Default payment terms to assign to orders when no other is provided." +msgstr "" +"Términos de pago por defecto para asignar a pedidos cuando no se proporciona" +" otro." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_quantity +msgid "" +"Default quantity to use for products that do not manage stock or when real " +"stock is not used." +msgstr "" +"Cantidad por defecto para usar en productos que no gestionan stock o cuando " +"no se usa el stock real." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__display_name +msgid "Display Name" +msgstr "Nombre para Mostrar" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_log__log_type__error +msgid "Error" +msgstr "Error" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__error_message +msgid "Error Message" +msgstr "Mensaje de Error" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_all_images +msgid "Export All Images" +msgstr "Exportar Todas las Imágenes" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_attribute_set_name +msgid "Export Attribute Set Name" +msgstr "Exportar Nombre del Conjunto de Atributos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_disabled_products +msgid "Export Disabled Products" +msgstr "Exportar Productos Deshabilitados" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_not_salable_products +msgid "Export Not Salable Products" +msgstr "Exportar Productos No Vendibles" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_only_selected +msgid "Export Only Selected" +msgstr "Exportar Solo los Seleccionados" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_out_of_stock +msgid "Export Out of Stock Products" +msgstr "Exportar Productos Sin Stock" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Export Product Types" +msgstr "Exportar Tipos de Productos" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Export Rules" +msgstr "Reglas de Exportación" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_product_product__export_to_shoppingfeed +msgid "Export To Shoppingfeed" +msgstr "Exportar a Shoppingfeed" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Exportable Products" +msgstr "Productos Exportables" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__exported_image_count +msgid "Exported Image Count" +msgstr "Cantidad de Imágenes Exportadas" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__failed +msgid "Failed" +msgstr "Fallido" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__field_id +msgid "Field" +msgstr "Campo" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (Socios)" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__force_zero_quantity_non_salable +msgid "Force Zero Quantity for Non Salable Products" +msgstr "Forzar Cantidad Cero para Productos No Vendibles" + +#. module: shoppingfeed_integration +#: model:res.groups,comment:shoppingfeed_integration.group_shoppingfeed_manager +msgid "Full access to configure and manage Shoppingfeed integration." +msgstr "Acceso completo para configurar y gestionar la integración de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Sync Account" +msgstr "Sincronizar Cuenta" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_type_goods +msgid "Goods" +msgstr "Mercancías" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__has_message +msgid "Has Message" +msgstr "Tiene Mensaje" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__id +msgid "ID" +msgstr "ID" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Si está marcado, los nuevos mensajes requieren su atención." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_has_error +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "Si está marcado, algunos mensajes tienen un error de entrega." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_all_images +msgid "" +"If enabled, all images of the product will be exported. If disabled, only " +"the main image or the number defined in 'Exported Image Count' will be " +"included in the feed." +msgstr "" +"Si está habilitado, se exportarán todas las imágenes del producto. Si está " +"deshabilitado, solo se incluirá la imagen principal o el número definido en " +"'Cantidad de Imágenes Exportadas' en el feed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_disabled_products +msgid "" +"If enabled, include archived (inactive) products in the Shoppingfeed export." +msgstr "" +"Si está habilitado, incluye productos archivados (inactivos) en la " +"exportación de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_not_salable_products +msgid "" +"If enabled, include products not allowed for sale (sale_ok = False) in the " +"Shoppingfeed export." +msgstr "" +"Si está habilitado, incluye productos no permitidos para la venta (sale_ok =" +" False) en la exportación de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_attribute_set_name +msgid "" +"If enabled, include the attribute set name in the exported catalog feed. " +"Currently has no effect unless a custom attribute set field exists." +msgstr "" +"Si está habilitado, incluye el nombre del conjunto de atributos en el feed " +"del catálogo exportado. Actualmente no tiene efecto a menos que exista un " +"campo de conjunto de atributos personalizado." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_only_selected +msgid "" +"If enabled, only products explicitly marked with 'Export To Shoppingfeed' " +"will be exported in the catalog feed." +msgstr "" +"Si está habilitado, solo los productos marcados explícitamente con 'Exportar" +" a Shoppingfeed' se exportarán en el feed del catálogo." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__import_orders +msgid "If enabled, orders from Shoppingfeed will be imported automatically." +msgstr "" +"Si está habilitado, los pedidos de Shoppingfeed se importarán " +"automáticamente." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__update_quantities_realtime +msgid "" +"If enabled, product quantities will be pushed to Shoppingfeed immediately " +"when stock changes are detected. This may slow down inventory updates." +msgstr "" +"Si está habilitado, las cantidades de productos se enviarán a Shoppingfeed " +"inmediatamente cuando se detecten cambios de stock. Esto puede ralentizar " +"las actualizaciones de inventario." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__force_zero_quantity_non_salable +msgid "" +"If enabled, products that are not allowed for sale (sale_ok=False) will be " +"exported with quantity 0." +msgstr "" +"Si está habilitado, los productos que no están permitidos para la venta " +"(sale_ok=False) se exportarán con cantidad 0." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_out_of_stock +msgid "" +"If enabled, products with no stock (quantity <= 0) will be included in the " +"Shoppingfeed export." +msgstr "" +"Si está habilitado, los productos sin stock (cantidad <= 0) se incluirán en " +"la exportación de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__use_actual_stock_state +msgid "" +"If enabled, real stock quantities will be used for products that manage " +"stock. Products without stock management will use the default quantity." +msgstr "" +"Si está habilitado, se usarán las cantidades de stock reales para productos " +"que gestionan stock. Los productos sin gestión de stock usarán la cantidad " +"por defecto." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__use_product_id_as_sku +msgid "" +"If enabled, the internal product ID will be used as the SKU in the feed, " +"instead of the product's Default Code." +msgstr "" +"Si está habilitado, se usará el ID interno del producto como SKU en el feed," +" en lugar del Código por Defecto del producto." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_product_product__export_to_shoppingfeed +msgid "" +"If enabled, this product will be exported to Shoppingfeed when the store is " +"configured to export only selected products." +msgstr "" +"Si está habilitado, este producto se exportará a Shoppingfeed cuando la " +"tienda esté configurada para exportar solo productos seleccionados." + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Images" +msgstr "Imágenes" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__import_orders +msgid "Import Orders" +msgstr "Importar Pedidos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_type_combo +msgid "Include combo products (type = 'combo') in the Shoppingfeed export." +msgstr "" +"Incluir productos combo (tipo = 'combo') en la exportación de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_type_goods +msgid "Include goods products (type = 'consu') in the Shoppingfeed export." +msgstr "" +"Incluir productos de mercancías (tipo = 'consu') en la exportación de " +"Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_type_service +msgid "" +"Include service products (type = 'service') in the Shoppingfeed export." +msgstr "" +"Incluir productos de servicios (tipo = 'service') en la exportación de " +"Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_stock_picking__shoppingfeed_shipped +msgid "Indicates if this picking has already been sent to ShoppingFeed" +msgstr "Indica si este albarán ya ha sido enviado a ShoppingFeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_channel_id +msgid "Indicates the channel from which this sales order originated." +msgstr "Indica el canal del cual se originó este pedido de venta." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_channel__upload_invoice_not_allowed +msgid "" +"Indicates whether this channel does NOT support invoice document upload. " +"Based on Shoppingfeed API documentation — see: https://docs.shopping-" +"feed.com/#order-operations-supported-per-channel" +msgstr "" +"Indica si este canal NO soporta la carga de documentos de factura. Basado en" +" la documentación de la API de Shoppingfeed — ver: https://docs.shopping-" +"feed.com/#order-operations-supported-per-channel" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_is_follower +msgid "Is Follower" +msgstr "Es Seguidor" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_account_move +msgid "Journal Entry" +msgstr "Asiento Contable" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__lang_id +msgid "Language" +msgstr "Idioma" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Leave empty to use field description" +msgstr "Dejar vacío para usar la descripción del campo" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__link +msgid "Link" +msgstr "Enlace" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__delivery_carrier_id +msgid "Local Odoo carrier equivalent for this Shoppingfeed carrier." +msgstr "Transportista local de Odoo equivalente para este transportista de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__log_type +msgid "Log Type" +msgstr "Tipo de Log" + +#. module: shoppingfeed_integration +#: model_terms:ir.actions.act_window,help:shoppingfeed_integration.action_shoppingfeed_log +msgid "" +"Logs are created automatically when the cron job imports orders from " +"Shoppingfeed." +msgstr "Los registros se crean automáticamente cuando el trabajo cron importa pedidos desde Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.module.category,description:shoppingfeed_integration.module_category_shoppingfeed +msgid "Manage Shoppingfeed integration" +msgstr "Gestionar integración de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:res.groups,name:shoppingfeed_integration.group_shoppingfeed_manager +msgid "Manager" +msgstr "Administrador" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__channel_id +msgid "Marketplace Channel" +msgstr "Canal del Marketplace" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store_order_type_map +msgid "Marketplace Customer Group Mapping" +msgstr "Asignación de Grupos de Clientes del Marketplace" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__marketplace_customer_group_ids +msgid "Marketplace Customer Groups" +msgstr "Grupos de Clientes del Marketplace" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_marketplace_orders +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_marketplace_orders +msgid "Marketplace Orders" +msgstr "Pedidos del Marketplace" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Marketplace Ref" +msgstr "Ref. Marketplace" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_reference +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_ticket__shoppingfeed_reference +msgid "Marketplace order reference (e.g., TEST-68fb749e8cd95)." +msgstr "Referencia del pedido del marketplace (ej., TEST-68fb749e8cd95)." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__message +msgid "Message" +msgstr "Mensaje" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_has_error +msgid "Message Delivery error" +msgstr "Error de Entrega de Mensaje" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__name +msgid "Name" +msgstr "Nombre" + +#. module: shoppingfeed_integration +#: model_terms:ir.actions.act_window,help:shoppingfeed_integration.action_shoppingfeed_ticket +msgid "No Shoppingfeed tickets found." +msgstr "No se encontraron tickets de Shoppingfeed." + +#. module: shoppingfeed_integration +#: model_terms:ir.actions.act_window,help:shoppingfeed_integration.action_shoppingfeed_log +msgid "No order logs found." +msgstr "No se encontraron registros de pedidos." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de Acciones" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_has_error_counter +msgid "Number of errors" +msgstr "Número de Errores" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__exported_image_count +msgid "" +"Number of images to export per product when 'Export All Images' is disabled." +msgstr "" +"Número de imágenes a exportar por producto cuando 'Exportar Todas las " +"Imágenes' está deshabilitado." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Número de mensajes que requieren acción" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de entrega" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_order_ref +msgid "Numeric order ID from Shoppingfeed API (e.g., 21786210256)." +msgstr "" +"ID numérico del pedido desde la API de Shoppingfeed (ej., 21786210256)." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__delivery_carrier_id +msgid "Odoo Delivery Carrier" +msgstr "Transportista de Entrega Odoo" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__order_id +msgid "Order" +msgstr "Pedido" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_log +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_log +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_log_list +msgid "Order Logs" +msgstr "Registros de Pedidos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__shoppingfeed_order_ref +msgid "Order Reference" +msgstr "Referencia del Pedido" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__order_type_id +msgid "Order Type" +msgstr "Tipo de Pedido" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Orders" +msgstr "Pedidos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__password +msgid "Password" +msgstr "Contraseña" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__pricelist_id +msgid "Pricelist for Shoppingfeed Export" +msgstr "Tarifa para Exportación de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__pricelist_id +msgid "Pricelist used to compute prices during Shoppingfeed export." +msgstr "Tarifa utilizada para calcular precios durante la exportación a Shoppingfeed." + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Prices" +msgstr "Precios" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_product_attribute +msgid "Product Attribute" +msgstr "Atributo de producto" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_product_product +msgid "Product Variant" +msgstr "Variante de producto" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__quantity_type +msgid "Quantity Type" +msgstr "Tipo de Cantidad" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_stock_quant +msgid "Quants" +msgstr "Cuantos" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__rating_ids +msgid "Ratings" +msgstr "Valoraciones" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_order_form_shoppingfeed +msgid "Raw Data" +msgstr "Datos Sin Procesar" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__result_data +msgid "Result Data" +msgstr "Datos del Resultado" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__running +msgid "Running" +msgstr "En Ejecución" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_has_sms_error +msgid "SMS Delivery error" +msgstr "Error de Entrega de SMS" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_store__quantity_type__salable +msgid "Salable Quantity" +msgstr "Cantidad Vendible" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_sale_order +msgid "Sales Order" +msgstr "Pedidos de Venta" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__scheduled +msgid "Scheduled" +msgstr "Programado" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__custom_sku_field_id +msgid "" +"Select field from product.product to be used as the SKU in the export. Leave" +" empty to use the default behavior (Default Code or Product ID)." +msgstr "" +"Seleccione el campo de product.product a usar como SKU en la exportación. " +"Déjelo vacío para usar el comportamiento por defecto (Código por Defecto o " +"ID del Producto)." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__additional_attribute_field_ids +msgid "" +"Select one or more fields from product.product to include as additional " +"attributes in the exported Shoppingfeed catalog. You can customize the " +"attribute name for each field." +msgstr "" +"Seleccione uno o más campos de product.product para incluir como atributos " +"adicionales en el catálogo exportado de Shoppingfeed. Útil para exportar " +"datos personalizados o metadatos adicionales." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_product_product__shoppingfeed_store_ids +msgid "Select the Shoppingfeed stores where this product should be exported." +msgstr "" +"Seleccione las tiendas de Shoppingfeed donde se debe exportar este producto." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__allowed_categ_ids +msgid "Select which product categories will be exported to Shoppingfeed." +msgstr "Seleccione qué categorías de productos se exportarán a Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_type_service +msgid "Services" +msgstr "Servicios" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Shipping" +msgstr "Envío" + +#. module: shoppingfeed_integration +#: model:ir.module.category,name:shoppingfeed_integration.module_category_shoppingfeed +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_root +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_order_form_shoppingfeed +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_product_form_inherit_shoppingfeed +msgid "Shoppingfeed" +msgstr "Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store_carrier_map +msgid "Shoppingfeed Carrier Mapping" +msgstr "Asignación de Transportistas de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__carrier_name +msgid "Shoppingfeed Carrier Name" +msgstr "Nombre del Transportista de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_channel +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_channel_id +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_channel_form +msgid "Shoppingfeed Channel" +msgstr "Canal de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_product_attribute__shoppingfeed_code_name_attribute +msgid "Shoppingfeed Code Name" +msgstr "Nombre de Código Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_order_ref +msgid "Shoppingfeed Order ID" +msgstr "ID del Pedido de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_log +msgid "Shoppingfeed Order Import Log" +msgstr "Registro de Importación de Pedidos Shoppingfeed" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_sale_order_filter_shoppingfeed +msgid "Shoppingfeed Orders" +msgstr "Pedidos de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_raw_data +msgid "Shoppingfeed Raw Data" +msgstr "Datos Sin Procesar de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_reference +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__shoppingfeed_reference +msgid "Shoppingfeed Reference" +msgstr "Referencia de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_stock_picking__shoppingfeed_shipped +msgid "Shoppingfeed Shipped" +msgstr "Enviado a Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_status +msgid "Shoppingfeed Status" +msgstr "Estado de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_store_id +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Shoppingfeed Store" +msgstr "Tienda de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store_attribute_field +msgid "Shoppingfeed Store Additional Attribute Field" +msgstr "Campo de Atributo Adicional de Tienda Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store +msgid "Shoppingfeed Store Configuration" +msgstr "Configuración de Tienda de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_store +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_product_product__shoppingfeed_store_ids +msgid "Shoppingfeed Stores" +msgstr "Tiendas de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_ticket +msgid "Shoppingfeed Ticket" +msgstr "Ticket de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_ticket +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Shoppingfeed Tickets" +msgstr "Tickets de Shoppingfeed" + +#. module: shoppingfeed_integration +#: model:ir.actions.server,name:shoppingfeed_integration.ir_cron_shoppingfeed_import_orders_ir_actions_server +msgid "Shoppingfeed: Import Orders" +msgstr "Shoppingfeed: Importar Pedidos" + +#. module: shoppingfeed_integration +#: model:ir.actions.server,name:shoppingfeed_integration.ir_cron_fetch_all_shoppingfeed_tickets_ir_actions_server +msgid "Shoppingfeed: Import Tickets" +msgstr "Shoppingfeed: Importar Tickets" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_sale_order_filter_shoppingfeed +msgid "Show only orders imported from Shoppingfeed." +msgstr "Mostrar solo pedidos importados desde Shoppingfeed." + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__state +msgid "State" +msgstr "Estado" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Stock" +msgstr "Stock" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__store_id +msgid "Store" +msgstr "Tienda" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__catalog_id +msgid "Store ID (Catalog)" +msgstr "ID de Tienda (Catálogo)" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Store Info" +msgstr "Información de la Tienda" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__status +msgid "Store Status" +msgstr "Estado de la Tienda" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__store_ids +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_store +msgid "Stores" +msgstr "Tiendas" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__succeed +msgid "Succeeded" +msgstr "Exitoso" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_log__log_type__success +msgid "Success" +msgstr "Éxito" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_store_id +msgid "The Shoppingfeed store from which this order was imported." +msgstr "La tienda de Shoppingfeed desde la cual se importó este pedido." + +#. module: shoppingfeed_integration +#: model:ir.model.constraint,message:shoppingfeed_integration.constraint_sale_order_unique_shoppingfeed_order_ref_by_company_store +msgid "This Shoppingfeed order has already been imported!" +msgstr "¡Este pedido de Shoppingfeed ya ha sido importado!" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Ticket ID" +msgstr "ID del Ticket" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__ticket_type +msgid "Ticket Type" +msgstr "Tipo de Ticket" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Ticket URL" +msgstr "URL del Ticket" + +#. module: shoppingfeed_integration +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_ticket +msgid "Tickets" +msgstr "Tickets" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__token_type +msgid "Token Type" +msgstr "Tipo de Token" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_stock_picking +msgid "Transfer" +msgstr "Traslado" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__update_quantities_realtime +msgid "Update Quantities in Real Time" +msgstr "Actualizar Cantidades en Tiempo Real" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__upload_invoice_not_allowed +msgid "Upload Invoice Not Allowed" +msgstr "Carga de Factura No Permitida" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__use_actual_stock_state +msgid "Use Actual Stock State" +msgstr "Usar Estado de Stock Real" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__use_product_id_as_sku +msgid "Use Product ID for SKU" +msgstr "Usar ID del Producto para SKU" + +#. module: shoppingfeed_integration +#: model:res.groups,name:shoppingfeed_integration.group_shoppingfeed_user +msgid "User" +msgstr "Usuario" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__username +msgid "Username" +msgstr "Nombre de Usuario" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_store__quantity_type__virtual +msgid "Virtual Quantity" +msgstr "Cantidad Virtual" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__website_message_ids +msgid "Website Messages" +msgstr "Mensajes del Sitio Web" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__website_message_ids +msgid "Website communication history" +msgstr "Historial de comunicación del sitio web" diff --git a/shoppingfeed_integration/i18n/shoppingfeed_integration.pot b/shoppingfeed_integration/i18n/shoppingfeed_integration.pot new file mode 100644 index 0000000..61a55ef --- /dev/null +++ b/shoppingfeed_integration/i18n/shoppingfeed_integration.pot @@ -0,0 +1,1089 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shoppingfeed_integration +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-01-22 11:54+0000\n" +"PO-Revision-Date: 2026-01-22 11:54+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__access_token +msgid "Access Token" +msgstr "" + +#. module: shoppingfeed_integration +#: model:res.groups,comment:shoppingfeed_integration.group_shoppingfeed_user +msgid "Access to view Shoppingfeed stores, channels, and tickets." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__email +msgid "Account Email" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Account Information" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__additional_attribute_field_ids +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Additional Attributes" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Attributes" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__canceled +msgid "Canceled" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__carrier_map_ids +msgid "Carrier Mappings" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__carrier_name +msgid "Carrier name as received from Shoppingfeed" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__feed_url +msgid "Catalog Feed URL" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Categories" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__allowed_categ_ids +msgid "Category Selection" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__channel_id +msgid "Channel" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_channel +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__channel_ids +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_channel +msgid "Channels" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__quantity_type +msgid "" +"Choose which stock quantity to export: 'Salable' uses qty_available, " +"'Virtual' uses virtual_available." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_type_combo +msgid "Combos" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__company_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__company_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__company_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__company_id +msgid "Company" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_raw_data +msgid "Complete JSON data received from Shoppingfeed API for this order." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__country_id +msgid "Country" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Created On" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__create_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__create_uid +msgid "Created by" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__create_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__create_date +msgid "Created on" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Credentials" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_status +msgid "Current status of the order in Shoppingfeed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__custom_name +msgid "Custom Attribute Name" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__custom_sku_field_id +msgid "Custom SKU Attribute" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_product_attribute__shoppingfeed_code_name_attribute +msgid "Custom attribute name used when exporting products to Shoppingfeed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__custom_name +msgid "" +"Custom name to use in the exported catalog. If empty, the field description " +"will be used." +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_log_list +msgid "Date" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_delivery_carrier_id +msgid "Default Delivery Carrier" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_order_type_id +msgid "Default Order Type" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_payment_mode_id +msgid "Default Payment Mode" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_payment_term_id +msgid "Default Payment Terms" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__default_quantity +msgid "Default Quantity" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_order_type_id +msgid "" +"Default Sale Order Type to assign to imported orders when no other type is " +"detected or explicitly set from the Shoppingfeed source." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_delivery_carrier_id +msgid "" +"Default carrier to assign to imported orders when the carrier from " +"Shoppingfeed is not mapped." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_payment_mode_id +msgid "Default payment mode to assign to orders when no other is provided." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_payment_term_id +msgid "Default payment terms to assign to orders when no other is provided." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__default_quantity +msgid "" +"Default quantity to use for products that do not manage stock or when real " +"stock is not used." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__display_name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__display_name +msgid "Display Name" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_log__log_type__error +msgid "Error" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__error_message +msgid "Error Message" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_all_images +msgid "Export All Images" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_attribute_set_name +msgid "Export Attribute Set Name" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_disabled_products +msgid "Export Disabled Products" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_not_salable_products +msgid "Export Not Salable Products" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_only_selected +msgid "Export Only Selected" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_out_of_stock +msgid "Export Out of Stock Products" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Export Product Types" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Export Rules" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_product_product__export_to_shoppingfeed +msgid "Export To Shoppingfeed" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Exportable Products" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__exported_image_count +msgid "Exported Image Count" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__failed +msgid "Failed" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__field_id +msgid "Field" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__force_zero_quantity_non_salable +msgid "Force Zero Quantity for Non Salable Products" +msgstr "" + +#. module: shoppingfeed_integration +#: model:res.groups,comment:shoppingfeed_integration.group_shoppingfeed_manager +msgid "Full access to configure and manage Shoppingfeed integration." +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Sync Account" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_type_goods +msgid "Goods" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__has_message +msgid "Has Message" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__id +msgid "ID" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_has_error +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_all_images +msgid "" +"If enabled, all images of the product will be exported. If disabled, only " +"the main image or the number defined in 'Exported Image Count' will be " +"included in the feed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_disabled_products +msgid "" +"If enabled, include archived (inactive) products in the Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_not_salable_products +msgid "" +"If enabled, include products not allowed for sale (sale_ok = False) in the " +"Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_attribute_set_name +msgid "" +"If enabled, include the attribute set name in the exported catalog feed. " +"Currently has no effect unless a custom attribute set field exists." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_only_selected +msgid "" +"If enabled, only products explicitly marked with 'Export To Shoppingfeed' " +"will be exported in the catalog feed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__import_orders +msgid "If enabled, orders from Shoppingfeed will be imported automatically." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__update_quantities_realtime +msgid "" +"If enabled, product quantities will be pushed to Shoppingfeed immediately " +"when stock changes are detected. This may slow down inventory updates." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__force_zero_quantity_non_salable +msgid "" +"If enabled, products that are not allowed for sale (sale_ok=False) will be " +"exported with quantity 0." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_out_of_stock +msgid "" +"If enabled, products with no stock (quantity <= 0) will be included in the " +"Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__use_actual_stock_state +msgid "" +"If enabled, real stock quantities will be used for products that manage " +"stock. Products without stock management will use the default quantity." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__use_product_id_as_sku +msgid "" +"If enabled, the internal product ID will be used as the SKU in the feed, " +"instead of the product's Default Code." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_product_product__export_to_shoppingfeed +msgid "" +"If enabled, this product will be exported to Shoppingfeed when the store is " +"configured to export only selected products." +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Images" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__import_orders +msgid "Import Orders" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_type_combo +msgid "Include combo products (type = 'combo') in the Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_type_goods +msgid "Include goods products (type = 'consu') in the Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__export_type_service +msgid "" +"Include service products (type = 'service') in the Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_stock_picking__shoppingfeed_shipped +msgid "Indicates if this picking has already been sent to ShoppingFeed" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_channel_id +msgid "Indicates the channel from which this sales order originated." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_channel__upload_invoice_not_allowed +msgid "" +"Indicates whether this channel does NOT support invoice document upload. " +"Based on Shoppingfeed API documentation — see: https://docs.shopping-" +"feed.com/#order-operations-supported-per-channel" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__lang_id +msgid "Language" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__write_uid +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__write_date +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__write_date +msgid "Last Updated on" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Leave empty to use field description" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__link +msgid "Link" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__delivery_carrier_id +msgid "Local Odoo carrier equivalent for this Shoppingfeed carrier." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__log_type +msgid "Log Type" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.actions.act_window,help:shoppingfeed_integration.action_shoppingfeed_log +msgid "" +"Logs are created automatically when the cron job imports orders from " +"Shoppingfeed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.module.category,description:shoppingfeed_integration.module_category_shoppingfeed +msgid "Manage Shoppingfeed integration" +msgstr "" + +#. module: shoppingfeed_integration +#: model:res.groups,name:shoppingfeed_integration.group_shoppingfeed_manager +msgid "Manager" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__channel_id +msgid "Marketplace Channel" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store_order_type_map +msgid "Marketplace Customer Group Mapping" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__marketplace_customer_group_ids +msgid "Marketplace Customer Groups" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_marketplace_orders +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_marketplace_orders +msgid "Marketplace Orders" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Marketplace Ref" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_reference +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_ticket__shoppingfeed_reference +msgid "Marketplace order reference (e.g., TEST-68fb749e8cd95)." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__message +msgid "Message" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_ids +msgid "Messages" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__name +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__name +msgid "Name" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.actions.act_window,help:shoppingfeed_integration.action_shoppingfeed_ticket +msgid "No Shoppingfeed tickets found." +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.actions.act_window,help:shoppingfeed_integration.action_shoppingfeed_log +msgid "No order logs found." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__exported_image_count +msgid "" +"Number of images to export per product when 'Export All Images' is disabled." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_order_ref +msgid "Numeric order ID from Shoppingfeed API (e.g., 21786210256)." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__delivery_carrier_id +msgid "Odoo Delivery Carrier" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__order_id +msgid "Order" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_log +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_log +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_log_list +msgid "Order Logs" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__shoppingfeed_order_ref +msgid "Order Reference" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__order_type_id +msgid "Order Type" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Orders" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__password +msgid "Password" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__pricelist_id +msgid "Pricelist for Shoppingfeed Export" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__pricelist_id +msgid "Pricelist used to compute prices during Shoppingfeed export." +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Prices" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_product_attribute +msgid "Product Attribute" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__quantity_type +msgid "Quantity Type" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_stock_quant +msgid "Quants" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__rating_ids +msgid "Ratings" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_order_form_shoppingfeed +msgid "Raw Data" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__result_data +msgid "Result Data" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__running +msgid "Running" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_store__quantity_type__salable +msgid "Salable Quantity" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__scheduled +msgid "Scheduled" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__custom_sku_field_id +msgid "" +"Select field from product.product to be used as the SKU in the export. Leave" +" empty to use the default behavior (Default Code or Product ID)." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__additional_attribute_field_ids +msgid "" +"Select one or more fields from product.product to include as additional " +"attributes in the exported Shoppingfeed catalog. You can customize the " +"attribute name for each field." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_product_product__shoppingfeed_store_ids +msgid "Select the Shoppingfeed stores where this product should be exported." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__allowed_categ_ids +msgid "Select which product categories will be exported to Shoppingfeed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__sequence +msgid "Sequence" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__export_type_service +msgid "Services" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Shipping" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.module.category,name:shoppingfeed_integration.module_category_shoppingfeed +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_root +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_order_form_shoppingfeed +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_product_form_inherit_shoppingfeed +msgid "Shoppingfeed" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store_carrier_map +msgid "Shoppingfeed Carrier Mapping" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__carrier_name +msgid "Shoppingfeed Carrier Name" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_channel +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_channel_id +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_channel_form +msgid "Shoppingfeed Channel" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_product_attribute__shoppingfeed_code_name_attribute +msgid "Shoppingfeed Code Name" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_order_ref +msgid "Shoppingfeed Order ID" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_log +msgid "Shoppingfeed Order Import Log" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_sale_order_filter_shoppingfeed +msgid "Shoppingfeed Orders" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_raw_data +msgid "Shoppingfeed Raw Data" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_reference +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__shoppingfeed_reference +msgid "Shoppingfeed Reference" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_stock_picking__shoppingfeed_shipped +msgid "Shoppingfeed Shipped" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_status +msgid "Shoppingfeed Status" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_sale_order__shoppingfeed_store_id +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Shoppingfeed Store" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store_attribute_field +msgid "Shoppingfeed Store Additional Attribute Field" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_store +msgid "Shoppingfeed Store Configuration" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_store +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_product_product__shoppingfeed_store_ids +msgid "Shoppingfeed Stores" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_shoppingfeed_ticket +msgid "Shoppingfeed Ticket" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.act_window,name:shoppingfeed_integration.action_shoppingfeed_ticket +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Shoppingfeed Tickets" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.server,name:shoppingfeed_integration.ir_cron_shoppingfeed_import_orders_ir_actions_server +msgid "Shoppingfeed: Import Orders" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.actions.server,name:shoppingfeed_integration.ir_cron_fetch_all_shoppingfeed_tickets_ir_actions_server +msgid "Shoppingfeed: Import Tickets" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_sale_order_filter_shoppingfeed +msgid "Show only orders imported from Shoppingfeed." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__state +msgid "State" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Stock" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_log__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_attribute_field__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_carrier_map__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store_order_type_map__store_id +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__store_id +msgid "Store" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__catalog_id +msgid "Store ID (Catalog)" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_store_form +msgid "Store Info" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__status +msgid "Store Status" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__store_ids +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_store +msgid "Stores" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_ticket__state__succeed +msgid "Succeeded" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_log__log_type__success +msgid "Success" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_sale_order__shoppingfeed_store_id +msgid "The Shoppingfeed store from which this order was imported." +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.constraint,message:shoppingfeed_integration.constraint_sale_order_unique_shoppingfeed_order_ref_by_company_store +msgid "This Shoppingfeed order has already been imported!" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Ticket ID" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_ticket__ticket_type +msgid "Ticket Type" +msgstr "" + +#. module: shoppingfeed_integration +#: model_terms:ir.ui.view,arch_db:shoppingfeed_integration.view_shoppingfeed_ticket_tree +msgid "Ticket URL" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.ui.menu,name:shoppingfeed_integration.menu_shoppingfeed_ticket +msgid "Tickets" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__token_type +msgid "Token Type" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model,name:shoppingfeed_integration.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__update_quantities_realtime +msgid "Update Quantities in Real Time" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_channel__upload_invoice_not_allowed +msgid "Upload Invoice Not Allowed" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__use_actual_stock_state +msgid "Use Actual Stock State" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__use_product_id_as_sku +msgid "Use Product ID for SKU" +msgstr "" + +#. module: shoppingfeed_integration +#: model:res.groups,name:shoppingfeed_integration.group_shoppingfeed_user +msgid "User" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__username +msgid "Username" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields.selection,name:shoppingfeed_integration.selection__shoppingfeed_store__quantity_type__virtual +msgid "Virtual Quantity" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,field_description:shoppingfeed_integration.field_shoppingfeed_store__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: shoppingfeed_integration +#: model:ir.model.fields,help:shoppingfeed_integration.field_shoppingfeed_store__website_message_ids +msgid "Website communication history" +msgstr "" diff --git a/shoppingfeed_integration/migrations/18.0.1.0.1/pre-migration.py b/shoppingfeed_integration/migrations/18.0.1.0.1/pre-migration.py new file mode 100644 index 0000000..3b33c11 --- /dev/null +++ b/shoppingfeed_integration/migrations/18.0.1.0.1/pre-migration.py @@ -0,0 +1,31 @@ +M2M_TABLE = "shoppingfeed_channel_shoppingfeed_store_rel" + + +def migrate(cr, version): + if not version: + return + cr.execute( + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = %s)", + (M2M_TABLE,), + ) + if not cr.fetchone()[0]: + return + cr.execute( + """ + ALTER TABLE shoppingfeed_channel + ADD COLUMN IF NOT EXISTS store_id INTEGER + REFERENCES shoppingfeed_store(id) ON DELETE CASCADE + """ + ) + cr.execute( + """ + UPDATE shoppingfeed_channel c + SET store_id = ( + SELECT shoppingfeed_store_id + FROM shoppingfeed_channel_shoppingfeed_store_rel + WHERE shoppingfeed_channel_id = c.id + LIMIT 1 + ) + """ + ) + cr.execute("DELETE FROM shoppingfeed_channel WHERE store_id IS NULL") diff --git a/shoppingfeed_integration/migrations/18.0.1.0.2/pre-migration.py b/shoppingfeed_integration/migrations/18.0.1.0.2/pre-migration.py new file mode 100644 index 0000000..05b14f3 --- /dev/null +++ b/shoppingfeed_integration/migrations/18.0.1.0.2/pre-migration.py @@ -0,0 +1,16 @@ +def migrate(cr, version): + if not version: + return + cr.execute( + """ + ALTER TABLE shoppingfeed_channel + ADD COLUMN IF NOT EXISTS sf_channel_name VARCHAR + """ + ) + cr.execute( + """ + UPDATE shoppingfeed_channel + SET sf_channel_name = name + WHERE sf_channel_name IS NULL + """ + ) diff --git a/shoppingfeed_integration/migrations/18.0.1.0.3/post-migration.py b/shoppingfeed_integration/migrations/18.0.1.0.3/post-migration.py new file mode 100644 index 0000000..8fa156c --- /dev/null +++ b/shoppingfeed_integration/migrations/18.0.1.0.3/post-migration.py @@ -0,0 +1,15 @@ +# Copyright 2026 Tecnativa - Carlos Lopez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.m2o_to_x2m( + env.cr, + env["shoppingfeed.store.carrier.map"], + "shoppingfeed_store_carrier_map", + "delivery_carrier_ids", + "delivery_carrier_id", + ) diff --git a/shoppingfeed_integration/migrations/18.0.1.0.4/post-migration.py b/shoppingfeed_integration/migrations/18.0.1.0.4/post-migration.py new file mode 100644 index 0000000..18a785b --- /dev/null +++ b/shoppingfeed_integration/migrations/18.0.1.0.4/post-migration.py @@ -0,0 +1,14 @@ +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + env.cr.execute( + """ + UPDATE sale_order + SET client_order_ref = shoppingfeed_reference + WHERE shoppingfeed_reference IS NOT NULL + AND shoppingfeed_reference != '' + AND (client_order_ref IS NULL OR client_order_ref = '') + """ + ) diff --git a/shoppingfeed_integration/models/__init__.py b/shoppingfeed_integration/models/__init__.py new file mode 100644 index 0000000..52bcb8b --- /dev/null +++ b/shoppingfeed_integration/models/__init__.py @@ -0,0 +1,15 @@ +from . import shoppingfeed_channel +from . import shoppingfeed_channel_country_carrier +from . import shoppingfeed_store_order_type_map +from . import shoppingfeed_store_carrier_map +from . import shoppingfeed_store_attribute_field +from . import shoppingfeed_store +from . import shoppingfeed_log +from . import product_template +from . import product_pricelist +from . import stock_quant +from . import sale_order +from . import stock_picking +from . import shoppingfeed_ticket +from . import account_move +from . import product_attribute diff --git a/shoppingfeed_integration/models/account_move.py b/shoppingfeed_integration/models/account_move.py new file mode 100644 index 0000000..4ac5398 --- /dev/null +++ b/shoppingfeed_integration/models/account_move.py @@ -0,0 +1,110 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import io +import json +import logging + +import requests + +from odoo import _, models + +_logger = logging.getLogger(__name__) + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _shoppingfeed_upload_invoice(self): + for move in self: + if move.move_type != "out_invoice": + continue + sale_orders = move.line_ids.sale_line_ids.order_id.filtered( + lambda so, move=move: so.company_id == move.company_id + ) + sale_order = sale_orders[:1] + if ( + not sale_order + or not sale_order.shoppingfeed_order_ref + or not sale_order.shoppingfeed_store_id + or not sale_order.shoppingfeed_channel_id + or sale_order.shoppingfeed_channel_id.upload_invoice_not_allowed + ): + continue + store = sale_order.shoppingfeed_store_id + if not store.access_token or not store.catalog_id: + continue + if store._shoppingfeed_is_demo_mode(): + continue + pdf_content, __ = self.env["ir.actions.report"]._render_qweb_pdf( + "account.account_invoices", move.ids + ) + pdf_file = io.BytesIO(pdf_content) + pdf_file.name = f"{move.name or 'invoice'}.pdf" + url = f"https://api.shopping-feed.com/v1/store/{store.catalog_id}/order/upload-documents" + headers = { + "Authorization": store.access_token, + } + payload = { + "order": [ + { + "id": int(sale_order.shoppingfeed_order_ref), + "documents": [{"type": "invoice"}], + } + ] + } + files = {"files[]": (pdf_file.name, pdf_file, "application/pdf")} + data = {"body": json.dumps(payload)} + response = requests.post( + url, headers=headers, files=files, data=data, timeout=30 + ) + if response.ok: + move.message_post( + body=_( + "Invoice uploaded to Shoppingfeed successfully " + "(order ref: %(ref)s, HTTP %(status)s).", + ref=sale_order.shoppingfeed_order_ref, + status=response.status_code, + ), + subtype_xmlid="mail.mt_note", + ) + else: + move.message_post( + body=_( + "Failed to upload invoice to Shoppingfeed " + "(order ref: %(ref)s, HTTP %(status)s): %(detail)s", + ref=sale_order.shoppingfeed_order_ref, + status=response.status_code, + detail=response.text, + ), + subtype_xmlid="mail.mt_note", + ) + + def _shoppingfeed_auto_pay(self): + for move in self: + if move.move_type != "out_invoice" or move.payment_state == "paid": + continue + sale_orders = move.line_ids.sale_line_ids.order_id.filtered( + lambda so, move=move: so.company_id == move.company_id + ) + sale_order = sale_orders[:1] + channel = sale_order.shoppingfeed_channel_id + if not sale_order or not channel or not channel.auto_pay: + continue + if ( + not move.preferred_payment_method_line_id + and channel.payment_method_line_id + ): + move.preferred_payment_method_line_id = channel.payment_method_line_id + if not move.preferred_payment_method_line_id: + continue + self.env["account.payment.register"].with_context( + active_model="account.move", + active_ids=move.ids, + ).create({})._create_payments() + + def _post(self, soft=True): + posted = super()._post(soft) + invoices = posted.filtered(lambda move: move.is_invoice()) + invoices._shoppingfeed_upload_invoice() + invoices._shoppingfeed_auto_pay() + return posted diff --git a/shoppingfeed_integration/models/product_attribute.py b/shoppingfeed_integration/models/product_attribute.py new file mode 100644 index 0000000..cc7bb5a --- /dev/null +++ b/shoppingfeed_integration/models/product_attribute.py @@ -0,0 +1,18 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ProductAttribute(models.Model): + _inherit = "product.attribute" + + shoppingfeed_code_name_attribute = fields.Char( + string="Shoppingfeed Code Name", + help="Custom attribute name used when exporting products to Shoppingfeed.", + ) + shoppingfeed_export = fields.Boolean( + string="Export to Shoppingfeed", + default=True, + help="If checked, this attribute will be included when generating the " + "catalog for Shoppingfeed.", + ) diff --git a/shoppingfeed_integration/models/product_pricelist.py b/shoppingfeed_integration/models/product_pricelist.py new file mode 100644 index 0000000..b11a490 --- /dev/null +++ b/shoppingfeed_integration/models/product_pricelist.py @@ -0,0 +1,14 @@ +# Copyright 2026 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ProductPricelist(models.Model): + _inherit = "product.pricelist" + + shoppingfeed_attribute_name = fields.Char( + help=( + "Name to use for this pricelist when exported as an attribute " + "in Shoppingfeed catalogs." + ), + ) diff --git a/shoppingfeed_integration/models/product_template.py b/shoppingfeed_integration/models/product_template.py new file mode 100644 index 0000000..e584999 --- /dev/null +++ b/shoppingfeed_integration/models/product_template.py @@ -0,0 +1,40 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + export_to_shoppingfeed = fields.Boolean( + default=False, + help="If enabled, this product will be exported to Shoppingfeed " + "when the store is configured to export only selected products.", + ) + shoppingfeed_store_ids = fields.Many2many( + comodel_name="shoppingfeed.store", + string="Shoppingfeed Stores", + help="Select the Shoppingfeed stores where this product should be exported.", + ) + sf_forced_category_id = fields.Many2one( + comodel_name="product.public.category", + string="Forced Category", + help="If set, this category will always be used when exporting to " + "Shoppingfeed, overriding the automatic category selection.", + ) + sf_forced_category_parent_id = fields.Many2one( + comodel_name="product.public.category", + string="Forced Category Parent", + compute="_compute_sf_forced_category_parent_id", + ) + + @api.depends("sf_forced_category_id", "sf_forced_category_id.parent_id") + def _compute_sf_forced_category_parent_id(self): + for rec in self: + cat = rec.sf_forced_category_id + rec.sf_forced_category_parent_id = cat.parent_id or cat + + def _shoppingfeed_update_pricing(self): + # TODO: implement real-time price push to Shoppingfeed API + pass diff --git a/shoppingfeed_integration/models/sale_order.py b/shoppingfeed_integration/models/sale_order.py new file mode 100644 index 0000000..b7af80a --- /dev/null +++ b/shoppingfeed_integration/models/sale_order.py @@ -0,0 +1,513 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import json +from datetime import datetime, timezone + +import requests +from markupsafe import Markup + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + shoppingfeed_order_ref = fields.Char( + string="Shoppingfeed Order ID", + help="Numeric order ID from Shoppingfeed API (e.g., 21786210256).", + copy=False, + index=True, + readonly=True, + ) + shoppingfeed_reference = fields.Char( + help="Marketplace order reference (e.g., TEST-68fb749e8cd95).", + copy=False, + index=True, + readonly=True, + ) + shoppingfeed_store_id = fields.Many2one( + comodel_name="shoppingfeed.store", + string="Shoppingfeed Store", + help="The Shoppingfeed store from which this order was imported.", + copy=False, + readonly=True, + index=True, + ) + shoppingfeed_channel_id = fields.Many2one( + comodel_name="shoppingfeed.channel", + string="Shoppingfeed Channel", + help="Indicates the channel from which this sales order originated.", + copy=False, + readonly=True, + ) + shoppingfeed_status = fields.Char( + readonly=True, + help="Current status of the order in Shoppingfeed.", + copy=False, + ) + shoppingfeed_raw_data = fields.Text( + readonly=True, + copy=False, + help="Complete JSON data received from Shoppingfeed API for this order.", + ) + + _sql_constraints = [ + ( + "unique_shoppingfeed_order_ref_by_company_store", + "unique(shoppingfeed_order_ref, company_id, shoppingfeed_store_id)", + "This Shoppingfeed order has already been imported!", + ), + ] + + def _is_shoppingfeed_disable_invoicing(self): + return self.shoppingfeed_channel_id.disable_invoicing + + @api.depends("shoppingfeed_channel_id") + def _compute_invoice_status(self): + res = super()._compute_invoice_status() + self.filtered(lambda so: so._is_shoppingfeed_disable_invoicing()).update( + {"invoice_status": "no"} + ) + return res + + @api.depends("shoppingfeed_channel_id") + def _compute_amount_to_invoice(self): + res = super()._compute_amount_to_invoice() + for so in self.filtered(lambda so: so._is_shoppingfeed_disable_invoicing()): + so.amount_to_invoice = 0 + return res + + def _shoppingfeed_fetch_orders(self, store): + # Fetch orders from Shoppingfeed for the given store. + url = f"https://api.shopping-feed.com/v1/store/{store.catalog_id}/order" + headers = { + "Authorization": store.access_token, + "Content-Type": "application/json", + } + params = { + "acknowledgment": store.filter_order_acknowledgment, + "limit": 200, + } + if store.filter_order_status: + params["status"] = store.filter_order_status + if store.date_download_since: + params["since"] = store.date_download_since.isoformat(timespec="seconds") + response = requests.get(url, headers=headers, params=params, timeout=30) + return response.json().get("_embedded", {}).get("order", []) + + @api.model + def _shoppingfeed_format_phone(self, phone_number, country): + if not phone_number: + return "" + phone_sanitized = self._phone_format( + number=phone_number, country=country, force_format="INTERNATIONAL" + ) + return phone_sanitized or phone_number + + def _shoppingfeed_prepare_partner( + self, billing, store, channel=None, additional_fields=None + ): + company = billing.get("company") or "" + if company: + name = company + is_company = True + else: + first = billing.get("firstName") or "" + last = billing.get("lastName") or "" + name = ( + f"{first} {last}".strip() + or billing.get("email") + or "Shoppingfeed Customer" + ) + is_company = False + af = additional_fields or {} + vat = ( + af.get("buyer_tax_registration_id") + or af.get("mms-customer-tax-id") + or af.get("buyer_identification_number") + or af.get("buyer_identifier_number") + ) + # Reuse an existing partner with the same VAT instead of duplicating it. + if vat: + existing = self.env["res.partner"].search( + [ + ("vat", "=", vat), + ("parent_id", "=", False), + ("company_id", "in", [store.company_id.id, False]), + ], + limit=1, + ) + if existing: + return existing + country = self.env["res.country"].search( + [("code", "=", billing.get("country"))], limit=1 + ) + phone = billing.get("phone") or billing.get("mobilePhone") + phone = self._shoppingfeed_format_phone(phone, country) + vals = { + "name": name, + "is_company": is_company, + "email": billing.get("email"), + "street": billing.get("street") or "", + "street2": billing.get("street2"), + "zip": billing.get("postalCode") or "", + "city": billing.get("city"), + "phone": phone, + "country_id": country.id, + "company_id": store.company_id.id, + } + vat_valid = False + if vat: + vals["vat"] = vat + if country: + vat_valid = self._shoppingfeed_valid_vat(vat, country, is_company) + if vat_valid: + if len(vat) > 1 and not vat[1].isalpha(): + vals["vat"] = f"{country.code}{vat}" + if channel and channel.account_id: + vals["property_account_receivable_id"] = channel.account_id.id + if channel and channel.payment_method_line_id: + vals["property_inbound_payment_method_line_id"] = ( + channel.payment_method_line_id.id + ) + return ( + self.env["res.partner"] + .with_context(no_vat_validation=not vat_valid) + .create(vals) + ) + + @api.model + def _shoppingfeed_valid_vat(self, vat, country, is_company): + """Overwrite by other modules to check valid vat methods""" + if self.env["res.partner"]._run_vat_test(vat, country, is_company) is False: + return False + return True + + def _shoppingfeed_prepare_shipping(self, shipping, partner, store): + # Prepare or create delivery address for the Shoppingfeed order. + if not shipping: + return None + country = self.env["res.country"].search( + [("code", "=", shipping.get("country"))], limit=1 + ) + phone = shipping.get("phone") or shipping.get("mobilePhone") + phone = self._shoppingfeed_format_phone(phone, country) + shipping_vals = { + "parent_id": partner.id, + "vat": partner.vat, + "type": "delivery", + "name": ( + f"{shipping.get('firstName', '')} " f"{shipping.get('lastName', '')}" + ).strip() + or "Shipping Address", + "street": shipping.get("street") or "", + "street2": shipping.get("street2"), + "zip": shipping.get("postalCode") or "", + "city": shipping.get("city"), + "phone": phone, + "email": shipping.get("email"), + "country_id": country.id, + "company_id": store.company_id.id, + } + shipping_partner = self.env["res.partner"].search( + [ + ("parent_id", "=", partner.id), + ("type", "=", "delivery"), + ("name", "=", shipping_vals["name"]), + ("street", "=", shipping_vals["street"]), + ("zip", "=", shipping_vals["zip"]), + ], + limit=1, + ) or self.env["res.partner"].create(shipping_vals) + return shipping_partner + + def _shoppingfeed_clean_product_reference(self, reference): + if not reference or "_" not in reference: + return reference + parts = reference.rsplit("_", 1) + if len(parts) == 2 and len(parts[1]) == 2: + return parts[0] + return reference + + def _shoppingfeed_resolve_product_reference(self, item_reference, aliases=None): + """Resolve product reference considering aliases from itemsReferencesAliases. + When a SKU is manually edited in ShoppingFeed, the old SKU remains in the + item reference but a mapping is created in itemsReferencesAliases pointing + to the new SKU. This method resolves the correct SKU to use. + """ + # Check if there's an alias for this reference + if aliases and item_reference in aliases: + return self._shoppingfeed_clean_product_reference(aliases[item_reference]) + # Return the cleaned original reference + return self._shoppingfeed_clean_product_reference(item_reference) + + def _shoppingfeed_acknowledge_order(self, store, order): + # Acknowledge the imported order to Shoppingfeed. + if store._shoppingfeed_is_demo_mode(): + return + url = f"https://api.shopping-feed.com/v1/store/{store.catalog_id}/order/acknowledge" + headers = { + "Authorization": store.access_token, + "Content-Type": "application/json", + } + payload = { + "order": [ + { + "id": int(order.get("id")), + "status": "success", + "acknowledgedAt": datetime.now(timezone.utc).isoformat(), + } + ] + } + requests.post(url, json=payload, headers=headers, timeout=30) + + def _create_shoppingfeed_log(self, store, order, channel, error_message): + self.env["shoppingfeed.log"].create( + { + "name": order.get("reference") or f"Order {order.get('id')}", + "store_id": store.id, + "channel_id": channel.id if channel else False, + "shoppingfeed_reference": order.get("reference"), + "error_message": error_message, + } + ) + + def _shoppingfeed_validate_products(self, order): + # Validate that all products exist before creating the order. + missing_products = [] + aliases = order.get("itemsReferencesAliases", {}) + for item in order.get("items", []): + ref = item.get("reference") + clean_ref = self._shoppingfeed_resolve_product_reference(ref, aliases) + product = self.env["product.product"].search( + [("default_code", "=", clean_ref)], limit=1 + ) + if not product: + missing_products.append(clean_ref) + return missing_products + + def _shoppingfeed_get_carrier( + self, store, channel, carrier_name, partner, shipping_partner=None + ): + carrier = False + shipping_partner = shipping_partner or partner + shipping_country = shipping_partner.country_id if shipping_partner else None + if carrier_name: + carrier_map = store.carrier_map_ids.filtered( + lambda m, carrier_name=carrier_name: m.carrier_name.strip().lower() + == carrier_name.strip().lower() + ) + if carrier_map: + for candidate_carrier in carrier_map.delivery_carrier_ids: + if candidate_carrier._match_address(shipping_partner): + carrier = candidate_carrier + break + if not carrier: + if channel and shipping_country and channel.country_carrier_ids: + country_rule = channel.country_carrier_ids.filtered( + lambda rule, country=shipping_country: rule.country_id == country + ) + if country_rule: + carrier = country_rule[0].delivery_carrier_id + if not carrier: + if channel and channel.default_delivery_carrier_id: + carrier = channel.default_delivery_carrier_id + elif store.default_delivery_carrier_id: + carrier = store.default_delivery_carrier_id + return carrier + + def _shoppingfeed_get_tax_data(self, sale_order, line_vals): + new_sol = self.env["sale.order.line"].new({**line_vals, "order_id": sale_order}) + tax_data = new_sol.tax_id.with_context(force_price_include=True).compute_all( + line_vals["price_unit"], + currency=sale_order.currency_id, + quantity=1.0, + partner=sale_order.partner_shipping_id, + ) + return tax_data + + def _shoppingfeed_prepare_order_line(self, store, item, new_order, aliases=None): + ref = item.get("reference") + clean_ref = self._shoppingfeed_resolve_product_reference(ref, aliases) + product = self.env["product.product"].search( + [("default_code", "=", clean_ref)], limit=1 + ) + if not product: + return False + price = item.get("price", product.list_price) + vals = { + "product_id": product.id, + "product_uom_qty": item.get("quantity", 1.0), + "price_unit": price, + "name": item.get("name") or product.display_name, + } + # TODO: Add condition to remove taxes depends on store or channel field + # if not store.include_taxes_in_price: + # return vals + tax_data = self._shoppingfeed_get_tax_data(new_order, vals) + vals["price_unit"] = tax_data["total_excluded"] + return vals + + def _shoppingfeed_create_sale_order( + self, + store, + order, + partner, + shipping_partner, + sf_channel, + order_type_id, + carrier, + ): + # Create sale order and lines from Shoppingfeed data. + ext_id = str(order.get("id")) + currency = self.env["res.currency"].search( + [("name", "=", order.get("payment", {}).get("currency", "EUR"))], + limit=1, + ) + aliases = order.get("itemsReferencesAliases", {}) + so_vals = { + "partner_id": partner.id, + "partner_invoice_id": partner.id, + "partner_shipping_id": shipping_partner.id + if shipping_partner + else partner.id, + "client_order_ref": order.get("reference"), + "shoppingfeed_reference": order.get("reference"), + "currency_id": currency.id, + "shoppingfeed_order_ref": ext_id, + "shoppingfeed_store_id": store.id, + "type_id": order_type_id or store.default_order_type_id.id, + "payment_mode_id": store.default_payment_mode_id.id, + "payment_term_id": store.default_payment_term_id.id, + "shoppingfeed_channel_id": sf_channel.id, + "shoppingfeed_status": order.get("status"), + "company_id": store.company_id.id, + "shoppingfeed_raw_data": json.dumps(order, indent=2, ensure_ascii=False), + } + new_so = self.new(so_vals) + order_lines = [] + for item in order.get("items", []): + line_vals = self._shoppingfeed_prepare_order_line( + store, item, new_so, aliases + ) + if line_vals: + order_lines.append((0, 0, line_vals)) + so_vals["order_line"] = order_lines + sale_order = self.create(so_vals) + if carrier: + payment = order.get("payment", {}) or {} + # TODO: Add condition to remove taxes depends on store or channel field + tax_data = self._shoppingfeed_get_tax_data( + new_so, + { + "product_id": carrier.product_id.id, + "price_unit": payment.get("shippingAmount", 0.0), + }, + ) + sale_order.set_delivery_line(carrier, tax_data["total_excluded"]) + link = ( + f"https://app.shopping-feed.com/v3/es/orders/detail/" + f"{order.get('id')}?store={order.get('storeId')}" + ) + body = Markup( + "

Order created automatically by Shoppingfeed.

" + f"

Marketplace: {sf_channel.name or 'Unknown'}

" + f"

" + "View order in Shoppingfeed

" + ) + sale_order.message_post( + body=body, message_type="comment", subtype_xmlid="mail.mt_note" + ) + return sale_order + + @api.model + def action_import_from_shoppingfeed(self): + stores = ( + self.env["shoppingfeed.store"] + .sudo() + .search( + [ + ("access_token", "!=", False), + ("catalog_id", "!=", False), + ] + ) + ) + for store in stores: + self.with_company(store.company_id)._import_orders_from_shoppingfeed(store) + + def _import_orders_from_shoppingfeed(self, store): + if not store.import_orders: + return + for order in self._shoppingfeed_fetch_orders(store): + ext_id = str(order.get("id")) + if self.search([("shoppingfeed_order_ref", "=", ext_id)], limit=1): + continue + channel = (order.get("_embedded") or {}).get("channel", {}) + channel_name = channel.get("name", "Unknown Marketplace") + sf_channel = ( + self.env["shoppingfeed.channel"] + .sudo() + .search( + [ + ("sf_channel_name", "=", channel_name), + ("store_id", "=", store.id), + ], + limit=1, + ) + ) + missing_products = self._shoppingfeed_validate_products(order) + if missing_products: + error_msg = f"Products not found: {', '.join(missing_products)}" + self._create_shoppingfeed_log(store, order, sf_channel, error_msg) + continue + try: + with self.env.cr.savepoint(): + billing = order.get("billingAddress", {}) or {} + additional_fields = order.get("additionalFields", {}) or {} + partner = self._shoppingfeed_prepare_partner( + billing, store, sf_channel, additional_fields + ) + shipping_partner = self._shoppingfeed_prepare_shipping( + order.get("shippingAddress", {}), partner, store + ) + # Type mapping + mapping = store.marketplace_customer_group_ids.filtered( + lambda m, + channel_name=channel_name: m.channel_id.sf_channel_name + == channel_name + ) + order_type_id = mapping.order_type_id.id if mapping else False + # Carrier mapping + shipment = order.get("shipment", {}) or {} + carrier_name = shipment.get("carrier") + carrier = self._shoppingfeed_get_carrier( + store, sf_channel, carrier_name, partner, shipping_partner + ) + new_sale = self._shoppingfeed_create_sale_order( + store, + order, + partner, + shipping_partner, + sf_channel, + order_type_id, + carrier, + ) + if sf_channel.auto_confirm_sale: + new_sale.action_confirm() + self._shoppingfeed_acknowledge_order(store, order) + except Exception as e: + error_msg = f"Error creating order: {str(e)}" + self._create_shoppingfeed_log(store, order, sf_channel, error_msg) + continue + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.depends("order_id.shoppingfeed_channel_id") + def _compute_invoice_status(self): + res = super()._compute_invoice_status() + self.filtered( + lambda sol: sol.order_id._is_shoppingfeed_disable_invoicing() + ).invoice_status = "no" + return res diff --git a/shoppingfeed_integration/models/shoppingfeed_channel.py b/shoppingfeed_integration/models/shoppingfeed_channel.py new file mode 100644 index 0000000..75088b2 --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_channel.py @@ -0,0 +1,111 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ShoppingfeedChannel(models.Model): + _name = "shoppingfeed.channel" + _description = "Shoppingfeed Channel" + _sql_constraints = [ + ( + "unique_sf_channel_name_per_store", + "UNIQUE(sf_channel_name, store_id)", + "API channel name must be unique per store.", + ) + ] + + name = fields.Char(required=True) + sf_channel_name = fields.Char( + readonly=True, + copy=False, + help=( + "Channel identifier as received from the Shoppingfeed API. " + "Used internally to match incoming orders to this channel. " + "Set automatically on account sync." + ), + ) + store_id = fields.Many2one( + comodel_name="shoppingfeed.store", + string="Store", + required=True, + ondelete="cascade", + ) + company_id = fields.Many2one( + comodel_name="res.company", + related="store_id.company_id", + store=True, + ) + default_delivery_carrier_id = fields.Many2one( + comodel_name="delivery.carrier", + string="Default Delivery Carrier", + help=( + "Default carrier to assign to imported orders from this channel " + "when the carrier from Shoppingfeed is not mapped. " + "If not set, the store's default carrier will be used instead." + ), + ) + country_carrier_ids = fields.One2many( + comodel_name="shoppingfeed.channel.country.carrier", + inverse_name="channel_id", + string="Carriers by Country", + help=( + "Override the default carrier based on the delivery country. " + "If the order's destination country matches one of these rules, " + "the specified carrier will be used instead of the channel default." + ), + ) + upload_invoice_not_allowed = fields.Boolean( + help=( + "Indicates whether this channel does NOT support invoice document upload. " + "Based on Shoppingfeed API documentation — see: " + "https://docs.shopping-feed.com/#order-operations-supported-per-channel" + ), + default=False, + ) + disable_invoicing = fields.Boolean( + help=( + "Mark this field if the channel " + "generates the invoice on your platform " + "and it is not necessary to invoice the sales order in Odoo." + ), + default=False, + ) + payment_method_line_id = fields.Many2one( + comodel_name="account.payment.method.line", + string="Payment Method", + domain="""[ + ('journal_id.active', '=', True), + ('payment_type', '=', 'inbound'), + ('company_id', 'parent_of', company_id), + ]""", + help=( + "Inbound payment method used to register the marketplace payment " + "when an order is imported from this channel. " + "The journal is derived from the selected payment method." + ), + ) + auto_pay = fields.Boolean( + string="Automatic Payment", + default=True, + help=( + "When enabled, invoices from this channel are automatically paid " + "upon confirmation using the configured payment method." + ), + ) + auto_confirm_sale = fields.Boolean( + string="Automatic Sale Confirmation", + default=True, + help=( + "When enabled, sales orders from this channel are automatically confirmed." + ), + ) + account_id = fields.Many2one( + comodel_name="account.account", + string="Receivable Account", + domain=[("account_type", "=", "asset_receivable")], + help=( + "Receivable account assigned to partners created from orders of this" + " channel." + ), + ) diff --git a/shoppingfeed_integration/models/shoppingfeed_channel_country_carrier.py b/shoppingfeed_integration/models/shoppingfeed_channel_country_carrier.py new file mode 100644 index 0000000..b9713d0 --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_channel_country_carrier.py @@ -0,0 +1,25 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ShoppingfeedChannelCountryCarrier(models.Model): + _name = "shoppingfeed.channel.country.carrier" + _description = "Shoppingfeed Channel Carrier by Country" + + channel_id = fields.Many2one( + comodel_name="shoppingfeed.channel", + required=True, + ondelete="cascade", + ) + country_id = fields.Many2one( + comodel_name="res.country", + string="Delivery Country", + required=True, + ) + delivery_carrier_id = fields.Many2one( + comodel_name="delivery.carrier", + string="Carrier", + required=True, + ) diff --git a/shoppingfeed_integration/models/shoppingfeed_log.py b/shoppingfeed_integration/models/shoppingfeed_log.py new file mode 100644 index 0000000..a1c1a53 --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_log.py @@ -0,0 +1,30 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ShoppingfeedLog(models.Model): + _name = "shoppingfeed.log" + _description = "Shoppingfeed Order Import Log" + _order = "create_date desc" + + name = fields.Char(required=True, index=True) + store_id = fields.Many2one( + comodel_name="shoppingfeed.store", + string="Store", + ondelete="cascade", + index=True, + ) + channel_id = fields.Many2one( + comodel_name="shoppingfeed.channel", + string="Channel", + ondelete="set null", + ) + company_id = fields.Many2one( + comodel_name="res.company", + related="store_id.company_id", + store=True, + readonly=True, + ) + shoppingfeed_reference = fields.Char(index=True) + error_message = fields.Text() diff --git a/shoppingfeed_integration/models/shoppingfeed_store.py b/shoppingfeed_integration/models/shoppingfeed_store.py new file mode 100644 index 0000000..3354367 --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_store.py @@ -0,0 +1,463 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import requests + +from odoo import api, fields, models +from odoo.exceptions import AccessError + + +class ShoppingfeedStore(models.Model): + _name = "shoppingfeed.store" + _inherit = ["mail.thread"] + _description = "Shoppingfeed Store Configuration" + + name = fields.Char() + country_id = fields.Many2one("res.country", string="Country") + company_id = fields.Many2one( + comodel_name="res.company", default=lambda self: self.env.company, required=True + ) + username = fields.Char(required=True) + password = fields.Char(required=True) + access_token = fields.Char(readonly=True) + token_type = fields.Char(readonly=True) + catalog_id = fields.Char(string="Store ID (Catalog)", tracking=True) + channel_ids = fields.One2many( + comodel_name="shoppingfeed.channel", + inverse_name="store_id", + string="Channels", + tracking=True, + ) + marketplace_customer_group_ids = fields.One2many( + comodel_name="shoppingfeed.store.order.type.map", + inverse_name="store_id", + string="Marketplace Customer Groups", + ) + lang_id = fields.Many2one("res.lang", string="Language", tracking=True) + email = fields.Char(string="Account Email") + status = fields.Char(string="Store Status", tracking=True) + website_id = fields.Many2one( + comodel_name="website", + string="Canonical Website", + help=( + "Website used to build product, category and image URLs in the " + "catalog feed (and the feed URL itself). If empty, the system " + "falls back to the global 'web.base.url' parameter and to the " + "host the request comes from." + ), + ) + feed_url = fields.Char( + string="Catalog Feed URL", + compute="_compute_feed_url", + readonly=True, + tracking=True, + ) + export_only_selected = fields.Boolean( + help="If enabled, only products explicitly marked with " + "'Export To Shoppingfeed' will be exported in the catalog feed.", + default=True, + tracking=True, + ) + export_type_goods = fields.Boolean( + string="Goods", + default=True, + help="Include goods products (type = 'consu') in the Shoppingfeed export.", + ) + export_type_service = fields.Boolean( + string="Services", + default=False, + tracking=True, + help="Include service products (type = 'service') in the Shoppingfeed export.", + ) + export_type_combo = fields.Boolean( + string="Combos", + default=False, + tracking=True, + help="Include combo products (type = 'combo') in the Shoppingfeed export.", + ) + export_out_of_stock = fields.Boolean( + string="Export Out of Stock Products", + default=True, + tracking=True, + help=( + "If enabled, products with no stock (quantity <= 0) will be included " + "in the Shoppingfeed export." + ), + ) + export_disabled_products = fields.Boolean( + default=False, + tracking=True, + help=( + "If enabled, include archived (inactive) products in the " + "Shoppingfeed export." + ), + ) + export_not_salable_products = fields.Boolean( + default=False, + tracking=True, + help=( + "If enabled, include products not allowed for sale (sale_ok = False) " + "in the Shoppingfeed export." + ), + ) + use_actual_stock_state = fields.Boolean( + default=True, + tracking=True, + help=( + "If enabled, real stock quantities will be used for products that " + "manage stock. Products without stock management will use the " + "default quantity." + ), + ) + quantity_type = fields.Selection( + [ + ("salable", "Salable Quantity"), + ("virtual", "Virtual Quantity"), + ], + default="salable", + tracking=True, + help=( + "Choose which stock quantity to export: " + "'Salable' uses qty_available, 'Virtual' uses virtual_available." + ), + ) + default_quantity = fields.Integer( + default=100, + tracking=True, + help=( + "Default quantity to use for products that do not manage stock or " + "when real stock is not used." + ), + ) + force_zero_quantity_non_salable = fields.Boolean( + string="Force Zero Quantity for Non Salable Products", + default=False, + tracking=True, + help=( + "If enabled, products that are not allowed for sale (sale_ok=False) " + "will be exported with quantity 0." + ), + ) + update_quantities_realtime = fields.Boolean( + string="Update Quantities in Real Time", + default=True, + tracking=True, + help=( + "If enabled, product quantities will be pushed to Shoppingfeed immediately " + "when stock changes are detected. " + "This may slow down inventory updates." + ), + ) + update_prices_realtime = fields.Boolean( + string="Update Prices in Real Time", + default=True, + tracking=True, + help=( + "If enabled, product prices will be pushed to Shoppingfeed immediately " + "when price changes are detected." + ), + ) + pricelist_id = fields.Many2one( + comodel_name="product.pricelist", + string="Pricelist for Shoppingfeed Export", + tracking=True, + help="Pricelist used to compute prices during Shoppingfeed export.", + ) + include_taxes_in_price = fields.Boolean( + string="Include Taxes in Price", + default=False, + tracking=True, + help="If enabled, the exported price will include customer taxes.", + ) + export_price_without_tax = fields.Boolean( + string="Export Price Without Tax as Attribute", + default=False, + tracking=True, + help="If enabled, the price without tax will be added as an attribute.", + ) + price_without_tax_attribute_name = fields.Char( + string="Attribute Name for Price Without Tax", + default="price_without_tax", + tracking=True, + help="Name of the attribute for the price without tax.", + ) + additional_pricelist_ids = fields.Many2many( + comodel_name="product.pricelist", + relation="shoppingfeed_store_pricelist_rel", + column1="store_id", + column2="pricelist_id", + string="Additional Pricelists", + tracking=True, + help="Additional pricelists to export as price attributes in the catalog.", + ) + use_product_id_as_sku = fields.Boolean( + string="Use Product ID for SKU", + default=False, + tracking=True, + help=( + "If enabled, the internal product ID will be used as the SKU in the feed, " + "instead of the product's Default Code." + ), + ) + custom_sku_field_id = fields.Many2one( + comodel_name="ir.model.fields", + string="Custom SKU Attribute", + domain="[('model', '=', 'product.product'), ('ttype', 'in', ['char', 'text'])]", + tracking=True, + help=( + "Select field from product.product to be used as the SKU in the export. " + "Leave empty to use the default behavior (Default Code or Product ID)." + ), + ) + additional_attribute_field_ids = fields.One2many( + comodel_name="shoppingfeed.store.attribute.field", + inverse_name="store_id", + string="Additional Attributes", + help=( + "Select one or more fields from product.product to include as " + "additional attributes in the exported Shoppingfeed catalog. " + "You can customize the attribute name for each field." + ), + ) + # TODO: Currently has no effect unless a custom attribute set field exists. + export_attribute_set_name = fields.Boolean( + default=False, + tracking=True, + help=( + "If enabled, include the attribute set name in the exported catalog feed. " + "Currently has no effect unless a custom attribute set field exists." + ), + ) + export_all_images = fields.Boolean( + default=True, + tracking=True, + help=( + "If enabled, all images of the product will be exported. " + "If disabled, only the main image or the number defined " + "in 'Exported Image Count' will be included in the feed." + ), + ) + exported_image_count = fields.Integer( + default=1, + tracking=True, + help=( + "Number of images to export per product when 'Export All Images' " + "is disabled." + ), + ) + allowed_categ_ids = fields.Many2many( + comodel_name="product.public.category", + string="Category Selection", + help="Select which product categories will be exported to Shoppingfeed.", + ) + import_orders = fields.Boolean( + default=True, + tracking=True, + help="If enabled, orders from Shoppingfeed will be imported automatically.", + ) + default_order_type_id = fields.Many2one( + comodel_name="sale.order.type", + string="Default Order Type", + tracking=True, + help=( + "Default Sale Order Type to assign to imported orders when no other type " + "is detected or explicitly set from the Shoppingfeed source." + ), + ) + default_payment_mode_id = fields.Many2one( + comodel_name="account.payment.mode", + string="Default Payment Mode", + tracking=True, + help="Default payment mode to assign to orders when no other is provided.", + ) + default_payment_term_id = fields.Many2one( + comodel_name="account.payment.term", + string="Default Payment Terms", + tracking=True, + help="Default payment terms to assign to orders when no other is provided.", + ) + carrier_map_ids = fields.One2many( + comodel_name="shoppingfeed.store.carrier.map", + inverse_name="store_id", + string="Carrier Mappings", + ) + default_delivery_carrier_id = fields.Many2one( + comodel_name="delivery.carrier", + string="Default Delivery Carrier", + help=( + "Default carrier to assign to imported orders " + "when the carrier from Shoppingfeed is not mapped." + ), + ) + notify_shipment = fields.Boolean( + string="Notify Shipment to ShoppingFeed", + default=True, + tracking=True, + help=( + "If enabled, shipment notifications will be sent to ShoppingFeed " + "when orders are shipped." + ), + ) + demo_mode = fields.Boolean( + default=False, + copy=False, + tracking=True, + help=( + "When active, ALL outgoing write requests to the ShoppingFeed API " + "are silently blocked: order acknowledgement, inventory updates, " + "shipment notifications and invoice uploads are suppressed. " + "Orders will still be imported from ShoppingFeed (GET requests " + "are not affected). Use this to test the integration without " + "affecting production data in ShoppingFeed." + ), + ) + filter_order_acknowledgment = fields.Selection( + [ + ("acknowledged", "Acknowledged"), + ("unacknowledged", "Unacknowledged"), + ], + default="unacknowledged", + string="Filter Acknowledgment", + ) + filter_order_status = fields.Selection( + [ + ("created", "Created"), + ("waiting_store_acceptance", "Waiting Store Acceptance"), + ("refused", "Refused"), + ("waiting_shipment", "Waiting Shipment"), + ("shipped", "Shipped"), + ("cancelled", "Cancelled"), + ("refunded", "Refunded"), + ("partially_refunded", "Partially Refunded"), + ("partially_shipped", "Partially Shipped"), + ], + default="waiting_shipment", + string="Filter Status", + help=( + "Only orders in this Shoppingfeed status will be imported. " + "Leave empty to import orders regardless of their status." + ), + ) + date_download_since = fields.Datetime() + + @api.depends("catalog_id", "website_id") + def _compute_feed_url(self): + for store in self: + base_url = store.get_base_url().rstrip("/") + store.feed_url = ( + f"{base_url}/catalog/{store.catalog_id}.xml" if store.catalog_id else "" + ) + + def action_toggle_demo_mode(self): + self.env["res.users"].check_access_rights("write") + if not self.env.user.has_group("base.group_system"): + raise AccessError( + self.env._( + "Only administrators can toggle the demo mode " + "on a Shoppingfeed store." + ) + ) + for store in self: + store.demo_mode = not store.demo_mode + + def _shoppingfeed_is_demo_mode(self): + self.ensure_one() + return bool(self.demo_mode) + + def action_get_access_token(self): + for store in self: + self._authenticate_with_shoppingfeed(store) + self._fetch_account_info(store) + + def _authenticate_with_shoppingfeed(self, store): + payload = { + "grant_type": "password", + "username": store.username, + "password": store.password, + } + headers = {"Content-Type": "application/json"} + auth_response = requests.post( + "https://api.shopping-feed.com/v1/auth", + json=payload, + headers=headers, + timeout=20, + ) + if auth_response.status_code != 200: + raise ValueError(f"Authentication failed: {auth_response.text}") + auth_data = auth_response.json() + access_token = auth_data.get("access_token") + token_type = auth_data.get("token_type") + if not access_token: + raise ValueError("No access token received from Shoppingfeed.") + store.write({"access_token": access_token, "token_type": token_type}) + + def _fetch_account_info(self, store): + # Retrieve store info, language, and channels from Shoppingfeed. + info_headers = { + "Content-Type": "application/json", + "Authorization": store.access_token, + } + info_response = requests.get( + "https://api.shopping-feed.com/v1/me", + headers=info_headers, + timeout=20, + ) + if info_response.status_code != 200: + raise ValueError(f"Failed to retrieve account info: {info_response.text}") + info_data = info_response.json() + self._update_store_from_info(store, info_data, info_headers) + + def _update_store_from_info(self, store, info_data, info_headers): + email = info_data.get("email") + embedded = info_data.get("_embedded", {}) + stores = embedded.get("store", []) + if not stores: + return + sf_store = stores[0] + catalog_id = str(sf_store.get("id")) + country_code = sf_store.get("country") + name = sf_store.get("name") + status = sf_store.get("status") + country = self.env["res.country"].search([("code", "=", country_code)], limit=1) + store.write( + { + "catalog_id": catalog_id, + "name": name or store.name, + "status": status, + "country_id": country.id if country else False, + "email": email, + } + ) + self._fetch_and_update_channels(store, sf_store, info_headers) + + def _fetch_and_update_channels(self, store, sf_store, info_headers): + # Fetch and update installed channels for this store. + channel_link = sf_store.get("_links", {}).get("channel", {}).get("href") + if not channel_link: + return + if not channel_link.startswith("http"): + channel_link = f"https://api.shopping-feed.com{channel_link}" + channel_response = requests.get(channel_link, headers=info_headers, timeout=20) + if channel_response.status_code != 200: + return + data = channel_response.json() + store_channels = data.get("_embedded", {}).get("storeChannel", []) + # Collect installed channel names from API + installed_names = { + channel_data["name"] + for sc in store_channels + if sc.get("installed") + for channel_data in [sc.get("_embedded", {}).get("channel", {})] + if channel_data.get("name") + } + Channel = self.env["shoppingfeed.channel"].sudo() + # Create channels that don't exist yet for this store (matched by API name) + existing_api_names = set(store.channel_ids.mapped("sf_channel_name")) + for api_name in installed_names: + if api_name not in existing_api_names: + Channel.create( + { + "name": api_name, + "sf_channel_name": api_name, + "store_id": store.id, + } + ) diff --git a/shoppingfeed_integration/models/shoppingfeed_store_attribute_field.py b/shoppingfeed_integration/models/shoppingfeed_store_attribute_field.py new file mode 100644 index 0000000..8664990 --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_store_attribute_field.py @@ -0,0 +1,32 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ShoppingfeedStoreAttributeField(models.Model): + _name = "shoppingfeed.store.attribute.field" + _description = "Shoppingfeed Store Additional Attribute Field" + _order = "sequence, id" + + store_id = fields.Many2one( + comodel_name="shoppingfeed.store", + string="Store", + required=True, + ondelete="cascade", + ) + field_id = fields.Many2one( + comodel_name="ir.model.fields", + string="Field", + required=True, + domain="[('model', '=', 'product.product'), " + "('ttype', 'in', ['char', 'text', 'integer', 'float', " + "'selection', 'many2one', 'boolean', 'html', 'datetime'])]", + ondelete="cascade", + ) + custom_name = fields.Char( + string="Custom Attribute Name", + help="Custom name to use in the exported catalog. " + "If empty, the field description will be used.", + ) + sequence = fields.Integer(default=10) diff --git a/shoppingfeed_integration/models/shoppingfeed_store_carrier_map.py b/shoppingfeed_integration/models/shoppingfeed_store_carrier_map.py new file mode 100644 index 0000000..938f13c --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_store_carrier_map.py @@ -0,0 +1,25 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ShoppingfeedStoreCarrierMap(models.Model): + _name = "shoppingfeed.store.carrier.map" + _description = "Shoppingfeed Carrier Mapping" + + store_id = fields.Many2one( + comodel_name="shoppingfeed.store", + ondelete="cascade", + required=True, + ) + carrier_name = fields.Char( + string="Shoppingfeed Carrier Name", + required=True, + help="Carrier name as received from Shoppingfeed", + ) + delivery_carrier_ids = fields.Many2many( + comodel_name="delivery.carrier", + string="Odoo Delivery Carrier", + required=True, + help="Local Odoo carrier equivalent for this Shoppingfeed carrier.", + ) diff --git a/shoppingfeed_integration/models/shoppingfeed_store_order_type_map.py b/shoppingfeed_integration/models/shoppingfeed_store_order_type_map.py new file mode 100644 index 0000000..218b3ef --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_store_order_type_map.py @@ -0,0 +1,23 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ShoppingfeedStoreOrderTypeMap(models.Model): + _name = "shoppingfeed.store.order.type.map" + _description = "Marketplace Customer Group Mapping" + + store_id = fields.Many2one( + comodel_name="shoppingfeed.store", ondelete="cascade", required=True + ) + channel_id = fields.Many2one( + comodel_name="shoppingfeed.channel", + string="Marketplace Channel", + required=True, + domain="[('id', 'in', parent.channel_ids)]", + ) + order_type_id = fields.Many2one( + comodel_name="sale.order.type", + string="Order Type", + required=True, + ) diff --git a/shoppingfeed_integration/models/shoppingfeed_ticket.py b/shoppingfeed_integration/models/shoppingfeed_ticket.py new file mode 100644 index 0000000..bf01136 --- /dev/null +++ b/shoppingfeed_integration/models/shoppingfeed_ticket.py @@ -0,0 +1,102 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import requests + +from odoo import api, fields, models + + +class ShoppingfeedTicket(models.Model): + _name = "shoppingfeed.ticket" + _description = "Shoppingfeed Ticket" + _order = "create_date desc" + + name = fields.Char(required=True, index=True) + store_id = fields.Many2one("shoppingfeed.store", required=True, ondelete="cascade") + order_id = fields.Many2one("sale.order") + shoppingfeed_reference = fields.Char( + help="Marketplace order reference (e.g., TEST-68fb749e8cd95).", + copy=False, + index=True, + readonly=True, + ) + ticket_type = fields.Char() + state = fields.Selection( + [ + ("scheduled", "Scheduled"), + ("running", "Running"), + ("succeed", "Succeeded"), + ("failed", "Failed"), + ("canceled", "Canceled"), + ], + default="scheduled", + ) + message = fields.Text() + result_data = fields.Text() + company_id = fields.Many2one( + comodel_name="res.company", + related="store_id.company_id", + store=True, + readonly=True, + ) + link = fields.Char() + + @api.model + def cron_fetch_all_tickets(self): + # Get all active Shoppingfeed stores with valid credentials + stores = ( + self.env["shoppingfeed.store"] + .sudo() + .search( + [ + ("access_token", "!=", False), + ("catalog_id", "!=", False), + ] + ) + ) + for store in stores: + url = f"https://api.shopping-feed.com/v1/store/{store.catalog_id}/ticket" + headers = { + "Authorization": store.access_token, + "Content-Type": "application/json", + } + response = requests.get(url, headers=headers, timeout=30) + if response.status_code != 200: + continue + data = response.json() or {} + tickets = data.get("_embedded", {}).get("ticket", []) + for t in tickets: + payload = t.get("payload", {}) or {} + reference = payload.get("reference") + order = ( + self.env["sale.order"] + .sudo() + .search( + [ + ("shoppingfeed_reference", "=", reference), + ("company_id", "=", store.company_id.id), + ], + limit=1, + ) + ) + existing = self.search( + [ + ("name", "=", t.get("id")), + ("store_id", "=", store.id), + ], + limit=1, + ) + vals = { + "store_id": store.id, + "order_id": order.id if order else False, + "shoppingfeed_reference": reference, + "ticket_type": t.get("type"), + "state": t.get("state"), + "message": t.get("result", {}).get("message"), + "result_data": str(t.get("result", {}).get("data")), + "link": t.get("_links", {}).get("self", {}).get("href", ""), + } + if existing: + existing.write(vals) + else: + vals["name"] = t.get("id") + self.create(vals) diff --git a/shoppingfeed_integration/models/stock_picking.py b/shoppingfeed_integration/models/stock_picking.py new file mode 100644 index 0000000..151e316 --- /dev/null +++ b/shoppingfeed_integration/models/stock_picking.py @@ -0,0 +1,76 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging + +import requests + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + shoppingfeed_shipped = fields.Boolean( + copy=False, + help="Indicates if this picking has already been sent to ShoppingFeed", + ) + + def _action_done(self): + res = super()._action_done() + self._shoppingfeed_notify_shipment() + return res + + def _shoppingfeed_ship_order_payload(self, sale_order): + self.ensure_one() + return { + "id": int(sale_order.shoppingfeed_order_ref), + "carrier": self.carrier_id.name or "Unknown", + "trackingLink": self.carrier_tracking_url or "", + "trackingNumber": self.carrier_tracking_ref or "", + } + + def _shoppingfeed_notify_shipment(self): + # Notify Shoppingfeed that the order has been shipped + for picking in self: + _logger.info("Envio de tracking a SF") + sale_order = picking.sale_id + if ( + picking.shoppingfeed_shipped + or not sale_order + or not sale_order.shoppingfeed_order_ref + or not sale_order.shoppingfeed_store_id + ): + _logger.info("Envio de tracking a SF 1er - contune") + continue + store = sale_order.shoppingfeed_store_id + if ( + not store.access_token + or not store.catalog_id + or not store.notify_shipment + ): + _logger.info("Envio de tracking a SF 2 - contune") + continue + if store._shoppingfeed_is_demo_mode(): + continue + order_payload = picking._shoppingfeed_ship_order_payload(sale_order) + if not order_payload: + _logger.info("Envio de tracking a SF 4 - contune") + continue + url = ( + f"https://api.shopping-feed.com/v1/store/{store.catalog_id}/order/ship" + ) + headers = { + "Authorization": store.access_token, + "Content-Type": "application/json", + } + payload = {"order": [order_payload]} + response = requests.post(url, json=payload, headers=headers, timeout=30) + _logger.info( + f"Envio de tracking a SF after " + f"response status: {response.status_code} " + f"response text: {response.text}" + ) + if response.status_code == 202: + picking.shoppingfeed_shipped = True diff --git a/shoppingfeed_integration/models/stock_quant.py b/shoppingfeed_integration/models/stock_quant.py new file mode 100644 index 0000000..853b8d7 --- /dev/null +++ b/shoppingfeed_integration/models/stock_quant.py @@ -0,0 +1,42 @@ +# Copyright 2025 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import requests + +from odoo import models + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + def write(self, vals): + res = super().write(vals) + if "quantity" in vals or "inventory_quantity" in vals: + self._shoppingfeed_update_inventory() + return res + + def _shoppingfeed_update_inventory(self): + for quant in self: + product = quant.product_id + if not product.default_code or not product.shoppingfeed_store_ids: + continue + stores = product.shoppingfeed_store_ids.filtered( + lambda store, product=product, quant=quant: ( + not store.export_only_selected or product.export_to_shoppingfeed + ) + and store.update_quantities_realtime + and store.company_id == quant.company_id + ) + quantity = int(product.qty_available) + for store in stores: + if store._shoppingfeed_is_demo_mode(): + continue + reference = f"{product.default_code}_{store.country_id.code}" + payload = { + "inventory": [{"reference": reference, "quantity": quantity}] + } + url = f"https://api.shopping-feed.com/v1/catalog/{store.catalog_id}/inventory" + headers = { + "Authorization": store.access_token, + "Content-Type": "application/json", + } + requests.put(url, headers=headers, json=payload, timeout=30) diff --git a/shoppingfeed_integration/pyproject.toml b/shoppingfeed_integration/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/shoppingfeed_integration/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/shoppingfeed_integration/readme/CONFIGURE.md b/shoppingfeed_integration/readme/CONFIGURE.md new file mode 100644 index 0000000..8483e36 --- /dev/null +++ b/shoppingfeed_integration/readme/CONFIGURE.md @@ -0,0 +1,87 @@ +## Store Setup + +### Create and authenticate your store + +1. Go to **Shoppingfeed → Stores** +2. Click **New** and fill: + - **Username**: Shoppingfeed API email + - **Password**: Shoppingfeed API password +3. Click **Get Token** button + - Automatically retrieves: catalog ID, channels, country, and language + +### Configure product export + +In the store form, configure these tabs: + +#### Exportable Products tab + +- **Export only selected**: Only export products with "Export to Shoppingfeed" enabled +- **Export Product Types**: Select product types to include (Goods, Services, Combos) +- **Export Rules**: Control which products to include: + - Out of stock products + - Archived products + - Non-salable products + +#### Stock tab + +- **Use actual stock state**: Use real quantities vs default quantity +- **Quantity type**: Salable (on-hand) or Virtual (forecasted) +- **Default quantity**: Quantity for products without stock tracking +- **Force zero quantity non salable**: Set 0 for non-salable products +- **Update quantities realtime**: Push stock changes immediately to Shoppingfeed + +#### Prices tab + +- **Pricelist**: Optional pricelist for computing export prices + +#### Attributes tab + +- **Use product ID as SKU**: Use product ID instead of default code +- **Custom SKU field**: Select custom field for SKU (optional) +- **Additional attribute fields**: Add custom product fields to export + +#### Images tab + +- **Export all images**: Include all product images +- **Exported image count**: Limit number of images (if not exporting all) + +#### Categories tab + +- **Allowed categories**: Restrict export to specific product categories + +#### Shipping tab + +- **Default delivery carrier**: Fallback carrier for unmapped carriers +- **Carrier mappings**: Map Shoppingfeed carrier names to Odoo carriers + - Add lines with Shoppingfeed carrier name and corresponding Odoo carrier + +#### Orders tab + +- **Import orders**: Enable automatic order import +- **Default payment term**: Payment terms for imported orders +- **Default payment mode**: Payment mode for imported orders +- **Default order type**: Order type for imported orders +- **Marketplace customer groups**: Map channels to specific order types + - Add lines with Channel and Order Type + +## Product Setup + +### Mark products for export + +1. Open a product +2. In **Shoppingfeed** tab: + - Enable **Export to Shoppingfeed** (if store requires it) + - Select **Shoppingfeed Stores** to export this product to + +**Note**: Products need default code (internal reference) to export. + +## Catalog Feed + +After configuration, your XML catalog feed is available at: + +``` +https://yourdomain.com/catalog/{catalog_id}.xml +``` + +Share this URL with Shoppingfeed for product synchronization. + diff --git a/shoppingfeed_integration/readme/CONTRIBUTORS.md b/shoppingfeed_integration/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..a4415b2 --- /dev/null +++ b/shoppingfeed_integration/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Tecnativa](https://www.tecnativa.com): + - Juan Carlos Oñate diff --git a/shoppingfeed_integration/readme/DESCRIPTION.md b/shoppingfeed_integration/readme/DESCRIPTION.md new file mode 100644 index 0000000..b963c8d --- /dev/null +++ b/shoppingfeed_integration/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module integrates Odoo with Shoppingfeed, a platform that connects your store with multiple marketplaces. + +* Export product catalogs to Shoppingfeed in XML format +* Import orders from connected marketplaces automatically +* Synchronize product prices and stock levels in real-time +* Map marketplace channels to Odoo sale order types +* Map delivery carriers from marketplaces to Odoo carriers +* Track Shoppingfeed tickets and notifications +* Configure export filters by product type, category, and stock availability diff --git a/shoppingfeed_integration/readme/USAGE.md b/shoppingfeed_integration/readme/USAGE.md new file mode 100644 index 0000000..c0b1c63 --- /dev/null +++ b/shoppingfeed_integration/readme/USAGE.md @@ -0,0 +1,30 @@ +## Product Updates + +### Price changes + +Product price updates send automatically to linked Shoppingfeed stores. + +### Stock changes + +Stock updates send automatically if store has **Update quantities realtime** enabled. + +## Orders + +### View orders + +Go to **Shoppingfeed → Marketplace Orders** to see marketplace orders. + +Orders show: +- Shoppingfeed Order ID +- Shoppingfeed Reference +- Shoppingfeed Store +- Shoppingfeed Channel (Amazon, eBay, etc.) +- Shoppingfeed Status + +### Process orders + +1. Confirm order +2. Validate delivery → shipment info sends to Shoppingfeed +3. Validate invoice → PDF uploads to Shoppingfeed (if channel supports it) + +Orders auto-acknowledge in Shoppingfeed on import. diff --git a/shoppingfeed_integration/security/ir.model.access.csv b/shoppingfeed_integration/security/ir.model.access.csv new file mode 100644 index 0000000..7f548c1 --- /dev/null +++ b/shoppingfeed_integration/security/ir.model.access.csv @@ -0,0 +1,17 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +shoppingfeed_integration.access_shoppingfeed_ticket_user,access_shoppingfeed_ticket_user,shoppingfeed_integration.model_shoppingfeed_ticket,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_ticket_manager,access_shoppingfeed_ticket_manager,shoppingfeed_integration.model_shoppingfeed_ticket,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_log_user,access_shoppingfeed_log_user,shoppingfeed_integration.model_shoppingfeed_log,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_log_manager,access_shoppingfeed_log_manager,shoppingfeed_integration.model_shoppingfeed_log,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_store_user,access_shoppingfeed_store_user,shoppingfeed_integration.model_shoppingfeed_store,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_store_manager,access_shoppingfeed_store_manager,shoppingfeed_integration.model_shoppingfeed_store,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_channel_user,access_shoppingfeed_channel_user,shoppingfeed_integration.model_shoppingfeed_channel,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_channel_manager,access_shoppingfeed_channel_manager,shoppingfeed_integration.model_shoppingfeed_channel,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_store_order_type_map_user,access_shoppingfeed_store_order_type_map_user,shoppingfeed_integration.model_shoppingfeed_store_order_type_map,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_store_order_type_map_manager,access_shoppingfeed_store_order_type_map_manager,shoppingfeed_integration.model_shoppingfeed_store_order_type_map,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_store_carrier_map_user,access_shoppingfeed_store_carrier_map_user,shoppingfeed_integration.model_shoppingfeed_store_carrier_map,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_store_carrier_map_manager,access_shoppingfeed_store_carrier_map_manager,shoppingfeed_integration.model_shoppingfeed_store_carrier_map,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_store_attribute_field_user,access_shoppingfeed_store_attribute_field_user,shoppingfeed_integration.model_shoppingfeed_store_attribute_field,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_store_attribute_field_manager,access_shoppingfeed_store_attribute_field_manager,shoppingfeed_integration.model_shoppingfeed_store_attribute_field,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 +shoppingfeed_integration.access_shoppingfeed_channel_country_carrier_user,access_shoppingfeed_channel_country_carrier_user,shoppingfeed_integration.model_shoppingfeed_channel_country_carrier,shoppingfeed_integration.group_shoppingfeed_user,1,0,0,0 +shoppingfeed_integration.access_shoppingfeed_channel_country_carrier_manager,access_shoppingfeed_channel_country_carrier_manager,shoppingfeed_integration.model_shoppingfeed_channel_country_carrier,shoppingfeed_integration.group_shoppingfeed_manager,1,1,1,1 diff --git a/shoppingfeed_integration/security/ir_rule.xml b/shoppingfeed_integration/security/ir_rule.xml new file mode 100644 index 0000000..e044d94 --- /dev/null +++ b/shoppingfeed_integration/security/ir_rule.xml @@ -0,0 +1,27 @@ + + + + Shoppingfeed Store: company restriction + + [('company_id', 'in', company_ids)] + + + + Shoppingfeed Channel: company restriction + + [('company_id', 'in', company_ids)] + + + + Shoppingfeed Ticket: company restriction + + [('company_id', 'in', company_ids)] + + + + Shoppingfeed Log: company restriction + + [('company_id', 'in', company_ids)] + + + diff --git a/shoppingfeed_integration/security/shoppingfeed_security.xml b/shoppingfeed_integration/security/shoppingfeed_security.xml new file mode 100644 index 0000000..e0c5b97 --- /dev/null +++ b/shoppingfeed_integration/security/shoppingfeed_security.xml @@ -0,0 +1,27 @@ + + + + Shoppingfeed + Manage Shoppingfeed integration + 100 + + + User + + + Access to view Shoppingfeed stores, channels, and tickets. + + + Manager + + + Full access to configure and manage Shoppingfeed integration. + + + + + diff --git a/shoppingfeed_integration/static/description/icon.png b/shoppingfeed_integration/static/description/icon.png new file mode 100644 index 0000000..0f85671 Binary files /dev/null and b/shoppingfeed_integration/static/description/icon.png differ diff --git a/shoppingfeed_integration/static/description/index.html b/shoppingfeed_integration/static/description/index.html new file mode 100644 index 0000000..b213ef1 --- /dev/null +++ b/shoppingfeed_integration/static/description/index.html @@ -0,0 +1,642 @@ + + + + + +Shoppingfeed Integration + + + +
+

Shoppingfeed Integration

+ + +

Beta License: AGPL-3 OCA/shoppingfeed Translate me on Weblate Try me on Runboat

+

This module integrates Odoo with Shoppingfeed, a platform that connects +your store with multiple marketplaces.

+
    +
  • Export product catalogs to Shoppingfeed in XML format
  • +
  • Import orders from connected marketplaces automatically
  • +
  • Synchronize product prices and stock levels in real-time
  • +
  • Map marketplace channels to Odoo sale order types
  • +
  • Map delivery carriers from marketplaces to Odoo carriers
  • +
  • Track Shoppingfeed tickets and notifications
  • +
  • Configure export filters by product type, category, and stock +availability
  • +
+

Table of contents

+ +
+

Configuration

+
+

Store Setup

+
+

Create and authenticate your store

+
    +
  1. Go to Shoppingfeed → Stores
  2. +
  3. Click New and fill:
      +
    • Username: Shoppingfeed API email
    • +
    • Password: Shoppingfeed API password
    • +
    +
  4. +
  5. Click Get Token button
      +
    • Automatically retrieves: catalog ID, channels, country, and +language
    • +
    +
  6. +
+
+
+

Configure product export

+

In the store form, configure these tabs:

+
+

Exportable Products tab

+
    +
  • Export only selected: Only export products with “Export to +Shoppingfeed” enabled
  • +
  • Export Product Types: Select product types to include (Goods, +Services, Combos)
  • +
  • Export Rules: Control which products to include:
      +
    • Out of stock products
    • +
    • Archived products
    • +
    • Non-salable products
    • +
    +
  • +
+
+
+

Stock tab

+
    +
  • Use actual stock state: Use real quantities vs default quantity
  • +
  • Quantity type: Salable (on-hand) or Virtual (forecasted)
  • +
  • Default quantity: Quantity for products without stock tracking
  • +
  • Force zero quantity non salable: Set 0 for non-salable products
  • +
  • Update quantities realtime: Push stock changes immediately to +Shoppingfeed
  • +
+
+
+

Prices tab

+
    +
  • Pricelist: Optional pricelist for computing export prices
  • +
+
+
+

Attributes tab

+
    +
  • Use product ID as SKU: Use product ID instead of default code
  • +
  • Custom SKU field: Select custom field for SKU (optional)
  • +
  • Additional attribute fields: Add custom product fields to export
  • +
+
+
+

Images tab

+
    +
  • Export all images: Include all product images
  • +
  • Exported image count: Limit number of images (if not exporting +all)
  • +
+
+
+

Categories tab

+
    +
  • Allowed categories: Restrict export to specific product categories
  • +
+
+
+

Shipping tab

+
    +
  • Default delivery carrier: Fallback carrier for unmapped carriers
  • +
  • Carrier mappings: Map Shoppingfeed carrier names to Odoo carriers
      +
    • Add lines with Shoppingfeed carrier name and corresponding Odoo +carrier
    • +
    +
  • +
+
+
+

Orders tab

+
    +
  • Import orders: Enable automatic order import
  • +
  • Default payment term: Payment terms for imported orders
  • +
  • Default payment mode: Payment mode for imported orders
  • +
  • Default order type: Order type for imported orders
  • +
  • Marketplace customer groups: Map channels to specific order types
      +
    • Add lines with Channel and Order Type
    • +
    +
  • +
+
+
+
+
+

Product Setup

+
+

Mark products for export

+
    +
  1. Open a product
  2. +
  3. In Shoppingfeed tab:
      +
    • Enable Export to Shoppingfeed (if store requires it)
    • +
    • Select Shoppingfeed Stores to export this product to
    • +
    +
  4. +
+

Note: Products need default code (internal reference) to export.

+
+
+
+

Catalog Feed

+

After configuration, your XML catalog feed is available at:

+
+https://yourdomain.com/catalog/{catalog_id}.xml
+
+

Share this URL with Shoppingfeed for product synchronization.

+
+
+
+

Usage

+
+

Product Updates

+
+

Price changes

+

Product price updates send automatically to linked Shoppingfeed stores.

+
+
+

Stock changes

+

Stock updates send automatically if store has Update quantities +realtime enabled.

+
+
+
+

Orders

+
+

View orders

+

Go to Shoppingfeed → Marketplace Orders to see marketplace orders.

+

Orders show:

+
    +
  • Shoppingfeed Order ID
  • +
  • Shoppingfeed Reference
  • +
  • Shoppingfeed Store
  • +
  • Shoppingfeed Channel (Amazon, eBay, etc.)
  • +
  • Shoppingfeed Status
  • +
+
+
+

Process orders

+
    +
  1. Confirm order
  2. +
  3. Validate delivery → shipment info sends to Shoppingfeed
  4. +
  5. Validate invoice → PDF uploads to Shoppingfeed (if channel supports +it)
  6. +
+

Orders auto-acknowledge in Shoppingfeed on import.

+
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

juancarlosonate-tecnativa

+

This module is part of the OCA/shoppingfeed project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/shoppingfeed_integration/tests/__init__.py b/shoppingfeed_integration/tests/__init__.py new file mode 100644 index 0000000..1ac6738 --- /dev/null +++ b/shoppingfeed_integration/tests/__init__.py @@ -0,0 +1 @@ +from . import test_shoppingfeed_integration diff --git a/shoppingfeed_integration/tests/test_shoppingfeed_integration.py b/shoppingfeed_integration/tests/test_shoppingfeed_integration.py new file mode 100644 index 0000000..ab1827f --- /dev/null +++ b/shoppingfeed_integration/tests/test_shoppingfeed_integration.py @@ -0,0 +1,237 @@ +# Copyright 2026 Juan Carlos Oñate - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import Command +from odoo.tests import tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestShoppingfeedIntegration(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.bank_journal = cls.company_data["default_journal_bank"] + cls.inbound_payment_method_line = cls.env["account.payment.method.line"].create( + { + "name": "SF Inbound Payment Method", + "payment_method_id": cls.bank_journal.available_payment_method_ids[ + 0 + ].id, + "payment_type": "inbound", + "journal_id": cls.bank_journal.id, + } + ) + cls.sf_store = cls.env["shoppingfeed.store"].create( + { + "name": "Test SF Store", + "username": "test_user", + "password": "test_pass", + "company_id": cls.env.company.id, + } + ) + cls.sf_channel = cls.env["shoppingfeed.channel"].create( + { + "name": "Test Channel", + "store_id": cls.sf_store.id, + "payment_method_line_id": cls.inbound_payment_method_line.id, + "auto_pay": True, + } + ) + cls.product_a.default_code = "TEST-SKU-001" + + @classmethod + def _sf_order_line(cls, product=None): + product = product or cls.product_a + return Command.create( + { + "product_id": product.id, + "product_uom_qty": 1.0, + "price_unit": product.list_price, + } + ) + + def test_partner_gets_payment_method_on_create(self): + """New partner created during SF import receives + property_inbound_payment_method_line_id from the channel.""" + billing = { + "email": "sf_new_customer@example.com", + "firstName": "SF", + "lastName": "Customer", + "street": "Calle Test 1", + "postalCode": "28001", + "city": "Madrid", + "country": "ES", + "phone": "600000000", + } + partner = self.env["sale.order"]._shoppingfeed_prepare_partner( + billing, self.sf_store, channel=self.sf_channel + ) + self.assertRecordValues( + partner, + [ + { + "property_inbound_payment_method_line_id": ( + self.inbound_payment_method_line.id + ) + } + ], + ) + + def test_billing_partner_dedup_by_vat(self): + SO = self.env["sale.order"] + billing = { + "company": "Amazon Business EU SARL", + "email": "ab@example.com", + "street": "38 avenue John F. Kennedy", + "postalCode": "L-1855", + "city": "Luxembourg", + "country": "LU", + } + af_de = {"buyer_tax_registration_id": "DE319514546"} + p1 = SO._shoppingfeed_prepare_partner( + billing, self.sf_store, channel=self.sf_channel, additional_fields=af_de + ) + p2 = SO._shoppingfeed_prepare_partner( + billing, self.sf_store, channel=self.sf_channel, additional_fields=af_de + ) + self.assertEqual(p1, p2) + p3 = SO._shoppingfeed_prepare_partner( + billing, + self.sf_store, + channel=self.sf_channel, + additional_fields={"buyer_tax_registration_id": "IT13397910962"}, + ) + self.assertNotEqual(p1, p3) + b2c = { + "firstName": "John", + "lastName": "Doe", + "email": "jd@example.com", + "country": "ES", + } + c1 = SO._shoppingfeed_prepare_partner( + b2c, self.sf_store, channel=self.sf_channel + ) + c2 = SO._shoppingfeed_prepare_partner( + b2c, self.sf_store, channel=self.sf_channel + ) + self.assertNotEqual(c1, c2) + + def test_invoice_auto_paid_on_post(self): + """Invoice from a SF order with auto_pay=True is paid on confirmation.""" + self.partner_a.property_inbound_payment_method_line_id = ( + self.inbound_payment_method_line + ) + sale_order = self.env["sale.order"].create( + { + "partner_id": self.partner_a.id, + "shoppingfeed_order_ref": "SF-TEST-AUTOPAY-001", + "shoppingfeed_store_id": self.sf_store.id, + "shoppingfeed_channel_id": self.sf_channel.id, + "order_line": [self._sf_order_line()], + } + ) + sale_order.action_confirm() + invoice = sale_order._create_invoices() + invoice.action_post() + self.assertEqual(invoice.payment_state, "paid") + + def test_invoice_not_auto_paid_when_disabled(self): + """Invoice is NOT auto-paid when channel has auto_pay=False.""" + self.sf_channel.auto_pay = False + self.partner_a.property_inbound_payment_method_line_id = ( + self.inbound_payment_method_line + ) + sale_order = self.env["sale.order"].create( + { + "partner_id": self.partner_a.id, + "shoppingfeed_order_ref": "SF-TEST-NOPAY-001", + "shoppingfeed_store_id": self.sf_store.id, + "shoppingfeed_channel_id": self.sf_channel.id, + "order_line": [self._sf_order_line()], + } + ) + sale_order.action_confirm() + invoice = sale_order._create_invoices() + invoice.action_post() + self.assertNotEqual(invoice.payment_state, "paid") + self.sf_channel.auto_pay = True + + def test_product_reference_cleaning(self): + """_shoppingfeed_clean_product_reference strips 2-char country suffixes only.""" + so = self.env["sale.order"] + self.assertEqual( + so._shoppingfeed_clean_product_reference("SKU123_ES"), "SKU123" + ) + self.assertEqual( + so._shoppingfeed_clean_product_reference("SKU123_FR"), "SKU123" + ) + self.assertEqual(so._shoppingfeed_clean_product_reference("SKU123"), "SKU123") + self.assertEqual( + so._shoppingfeed_clean_product_reference("SKU123_EUR"), "SKU123_EUR" + ) + + def test_product_reference_resolution_with_alias(self): + """_shoppingfeed_resolve_product_reference follows alias mapping.""" + so = self.env["sale.order"] + aliases = {"OLD-SKU": "NEW-SKU_ES"} + self.assertEqual( + so._shoppingfeed_resolve_product_reference("OLD-SKU", aliases), "NEW-SKU" + ) + self.assertEqual( + so._shoppingfeed_resolve_product_reference("DIRECT_ES", aliases), "DIRECT" + ) + + def test_validate_products_all_found(self): + """_shoppingfeed_validate_products returns empty list when all SKUs exist.""" + order = { + "items": [{"reference": "TEST-SKU-001"}], + "itemsReferencesAliases": {}, + } + self.assertEqual( + self.env["sale.order"]._shoppingfeed_validate_products(order), [] + ) + + def test_validate_products_missing_sku(self): + """_shoppingfeed_validate_products returns missing SKUs.""" + order = { + "items": [{"reference": "NONEXISTENT-SKU"}], + "itemsReferencesAliases": {}, + } + missing = self.env["sale.order"]._shoppingfeed_validate_products(order) + self.assertIn("NONEXISTENT-SKU", missing) + + def test_create_sale_order_from_sf_data(self): + """_shoppingfeed_create_sale_order creates a sale.order with SF metadata.""" + order_dict = { + "id": "SF-999001", + "reference": "MKT-TEST-REF-001", + "status": "waiting_shipment", + "payment": {"currency": "EUR", "shippingAmount": 0.0}, + "items": [], + "itemsReferencesAliases": {}, + "storeId": "test", + } + sale_order = self.env["sale.order"]._shoppingfeed_create_sale_order( + self.sf_store, + order_dict, + self.partner_a, + None, + self.sf_channel, + False, + False, + ) + self.assertRecordValues( + sale_order, + [ + { + "shoppingfeed_order_ref": "SF-999001", + "shoppingfeed_reference": "MKT-TEST-REF-001", + "client_order_ref": "MKT-TEST-REF-001", + "shoppingfeed_channel_id": self.sf_channel.id, + "shoppingfeed_store_id": self.sf_store.id, + "partner_id": self.partner_a.id, + } + ], + ) diff --git a/shoppingfeed_integration/views/menus.xml b/shoppingfeed_integration/views/menus.xml new file mode 100644 index 0000000..76307fc --- /dev/null +++ b/shoppingfeed_integration/views/menus.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/shoppingfeed_integration/views/product_attribute_views.xml b/shoppingfeed_integration/views/product_attribute_views.xml new file mode 100644 index 0000000..ab22a12 --- /dev/null +++ b/shoppingfeed_integration/views/product_attribute_views.xml @@ -0,0 +1,14 @@ + + + + product.attribute.form.shoppingfeed + product.attribute + + + + + + + + + diff --git a/shoppingfeed_integration/views/product_pricelist_views.xml b/shoppingfeed_integration/views/product_pricelist_views.xml new file mode 100644 index 0000000..70dc2bd --- /dev/null +++ b/shoppingfeed_integration/views/product_pricelist_views.xml @@ -0,0 +1,13 @@ + + + + product.pricelist.form.inherit.shoppingfeed + product.pricelist + + + + + + + + diff --git a/shoppingfeed_integration/views/product_template_views.xml b/shoppingfeed_integration/views/product_template_views.xml new file mode 100644 index 0000000..ff73978 --- /dev/null +++ b/shoppingfeed_integration/views/product_template_views.xml @@ -0,0 +1,33 @@ + + + + product.template.form.shoppingfeed + product.template + + +
+ + + +
+ + + + + + + + + +
+
+
diff --git a/shoppingfeed_integration/views/sale_order_views.xml b/shoppingfeed_integration/views/sale_order_views.xml new file mode 100644 index 0000000..3f08092 --- /dev/null +++ b/shoppingfeed_integration/views/sale_order_views.xml @@ -0,0 +1,69 @@ + + + + sale.order.form.shoppingfeed + sale.order + + + + + + + + + + + + + + + + + + + + sale.order.tree.shoppingfeed + sale.order + + + + hide + + + hide + + + hide + + + + + + + + + + Marketplace Orders + sale.order + list,form + [('shoppingfeed_order_ref', '!=', False)] + {} + + + sale.order.search.shoppingfeed + sale.order + + + + + + + + + + diff --git a/shoppingfeed_integration/views/shoppingfeed_channel_views.xml b/shoppingfeed_integration/views/shoppingfeed_channel_views.xml new file mode 100644 index 0000000..fd185db --- /dev/null +++ b/shoppingfeed_integration/views/shoppingfeed_channel_views.xml @@ -0,0 +1,65 @@ + + + + shoppingfeed.channel.tree + shoppingfeed.channel + + + + + + + + + + + shoppingfeed.channel.form + shoppingfeed.channel + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Channels + shoppingfeed.channel + list,form + +
diff --git a/shoppingfeed_integration/views/shoppingfeed_log_views.xml b/shoppingfeed_integration/views/shoppingfeed_log_views.xml new file mode 100644 index 0000000..c1d5015 --- /dev/null +++ b/shoppingfeed_integration/views/shoppingfeed_log_views.xml @@ -0,0 +1,22 @@ + + + + shoppingfeed.log.list + shoppingfeed.log + + + + + + + + + + + + + Order Logs + shoppingfeed.log + list + + diff --git a/shoppingfeed_integration/views/shoppingfeed_store_views.xml b/shoppingfeed_integration/views/shoppingfeed_store_views.xml new file mode 100644 index 0000000..d5222e6 --- /dev/null +++ b/shoppingfeed_integration/views/shoppingfeed_store_views.xml @@ -0,0 +1,249 @@ + + + + shoppingfeed.store.tree + shoppingfeed.store + + + + + + + + + + + + + + shoppingfeed.store.form + shoppingfeed.store + +
+
+
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + Shoppingfeed Stores + shoppingfeed.store + list,form + +
diff --git a/shoppingfeed_integration/views/shoppingfeed_ticket_views.xml b/shoppingfeed_integration/views/shoppingfeed_ticket_views.xml new file mode 100644 index 0000000..76488aa --- /dev/null +++ b/shoppingfeed_integration/views/shoppingfeed_ticket_views.xml @@ -0,0 +1,40 @@ + + + + shoppingfeed.ticket.tree + shoppingfeed.ticket + + + + + + + + + + + + + + + + + Shoppingfeed Tickets + shoppingfeed.ticket + list + +

+ No Shoppingfeed tickets found. +

+
+
+