Repository: softwareone-platform/swo-marketplace-cli
Pull request #52: MPT-9934: add data parser models
Author: login: svazquezco
Created: 2025-05-26T18:00:57Z | Merged: 2025-05-30T10:54:59Z

Continuing with -> https://github.com/softwareone-platform/swo-marketplace-cli/pull/50. We introduced data models to standardize how data is handled between the layers. Each `DataModel` implements `from_dict` and `to_json` methods, encapsulating logic inside the class.


### Changes
- First iteration implementing the abstract class `BaseDataModel` and concrete implementations for each model (Product, Parameter, PriceList, ParameterGroup, ...)
- Replace existing logic to use the specific `DataModel`.


### Next Step
- Implement Services to handle the create, update, sync,... logic.

### Notes
- StatsCollectors were improved by implementing a `to_table` method for table representation.

While making this refactor:
- We will add tests for the new code.
- Existing tests will only be updated as needed to ensure the current logic continues working as expected. They will be removed or changed once the full flow has been implemented.
- Some logic handle the `DataModels` will be further improved when the Services are implemented


Diff:
diff --git a/swo/mpt/cli/core/models/__init__.py b/swo/mpt/cli/core/models/__init__.py
new file mode 100644
index 00000000..cb7a8a8d
--- /dev/null
+++ b/swo/mpt/cli/core/models/__init__.py
@@ -0,0 +1,3 @@
+from .data_model import BaseDataModel
+
+__all__ = ["BaseDataModel"]
diff --git a/swo/mpt/cli/core/models/data_model.py b/swo/mpt/cli/core/models/data_model.py
new file mode 100644
index 00000000..ebbe4af9
--- /dev/null
+++ b/swo/mpt/cli/core/models/data_model.py
@@ -0,0 +1,17 @@
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import Any, TypeVar
+
+DataModel = TypeVar("DataModel", bound="BaseDataModel")
+
+
+@dataclass
+class BaseDataModel(ABC):
+    @classmethod
+    @abstractmethod
+    def from_dict(cls, data: dict[str, Any]) -> "BaseDataModel":
+        raise NotImplementedError
+
+    @abstractmethod
+    def to_json(self) -> dict[str, Any]:
+        raise NotImplementedError
diff --git a/swo/mpt/cli/core/mpt/flows.py b/swo/mpt/cli/core/mpt/flows.py
index c604717a..ff3890e1 100644
--- a/swo/mpt/cli/core/mpt/flows.py
+++ b/swo/mpt/cli/core/mpt/flows.py
@@ -85,10 +85,7 @@ def create_product(
     products_response.raise_for_status()
     product = Product.model_validate(products_response.json())
 
-    response = mpt_client.put(
-        f"/catalog/products/{product.id}/settings",
-        json=settings_json,
-    )
+    response = mpt_client.put(f"/catalog/products/{product.id}/settings", json=settings_json)
     response.raise_for_status()
 
     return product
@@ -163,10 +160,7 @@ def get_item(mpt_client: MPTClient, product_id: str, vendor_id: str) -> Item:
 
     data = response.json()["data"]
     if not data:
-        raise MPTAPIError(
-            f"Item by Item Vendor ID '{vendor_id}' is not found.",
-            "404 not found",
-        )
+        raise MPTAPIError(f"Item by Item Vendor ID '{vendor_id}' is not found.", "404 not found")
 
     return Item.model_validate(data[0])
 
@@ -179,10 +173,7 @@ def search_uom_by_name(mpt_client: MPTClient, uom_name: str) -> Uom:
 
     data = response.json()["data"]
     if not data:
-        raise MPTAPIError(
-            f"Unit of measure by name '{uom_name}' is not found.",
-            "404 not found",
-        )
+        raise MPTAPIError(f"Unit of measure by name '{uom_name}' is not found.", "404 not found")
 
     return Uom.model_validate(data[0])
 
@@ -244,8 +235,7 @@ def update_pricelist_item(
     pricelist_item_json: dict,
 ) -> PricelistItem:
     response = mpt_client.put(
-        f"/catalog/price-lists/{pricelist_id}/items/{pricelist_item_id}",
-        json=pricelist_item_json,
+        f"/catalog/price-lists/{pricelist_id}/items/{pricelist_item_id}", json=pricelist_item_json
     )
     response.raise_for_status()
 
diff --git a/swo/mpt/cli/core/pricelists/app.py b/swo/mpt/cli/core/pricelists/app.py
index b75d8b6f..2b14075e 100644
--- a/swo/mpt/cli/core/pricelists/app.py
+++ b/swo/mpt/cli/core/pricelists/app.py
@@ -3,17 +3,10 @@
 from typing import Annotated
 
 import typer
-from rich import box
-from rich.table import Table
 from swo.mpt.cli.core.accounts.app import get_active_account
 from swo.mpt.cli.core.console import console
 from swo.mpt.cli.core.mpt.client import client_from_account
-from swo.mpt.cli.core.mpt.models import Pricelist
-from swo.mpt.cli.core.pricelists.flows import (
-    PricelistAction,
-    check_pricelist,
-    sync_pricelist,
-)
+from swo.mpt.cli.core.pricelists.flows import PricelistAction, check_pricelist, sync_pricelist
 from swo.mpt.cli.core.stats import PricelistStatsCollector
 
 app = typer.Typer()
@@ -83,10 +76,8 @@ def sync_pricelists(
         has_error = has_error or stats.is_error
 
         if pricelist:
-            pricelist_table = _pricelist_stats_table(pricelist, stats)
-            pricelist_table = _list_pricelist_stats(pricelist_table, stats)
-
-            console.print(pricelist_table)
+            stats.pricelist_id = pricelist.id
+            console.print(stats.to_table())
         else:
             console.print("Pricelist sync [red bold]FAILED")
 
@@ -94,34 +85,5 @@ def sync_pricelists(
         raise typer.Exit(code=4)
 
 
-def _pricelist_stats_table(pricelist: Pricelist, stats: PricelistStatsCollector) -> Table:
-    if stats.is_error:
-        title = "Pricelist sync [red bold]FAILED"
-    else:
-        title = f"Pricelist {pricelist.id} sync [green bold]SUCCEED"
-
-    table = Table(title, box=box.ROUNDED)
-
-    table.add_column("Total")
-    table.add_column("Synced")
-    table.add_column("Errors")
-    table.add_column("Skipped")
-
-    return table
-
-
-def _list_pricelist_stats(table: Table, stats: PricelistStatsCollector) -> Table:
-    for tab_name, tab_stats in stats.tabs.items():
-        table.add_row(
-            tab_name,
-            f"[blue]{tab_stats['total']}",
-            f"[green]{tab_stats['synced']}",
-            f"[red bold]{tab_stats['error']}",
-            f"[white]{tab_stats['skipped']}",
-        )
-
-    return table
-
-
 if __name__ == "__main__":
     app()
diff --git a/swo/mpt/cli/core/pricelists/flows.py b/swo/mpt/cli/core/pricelists/flows.py
index 34569c65..8a07e8da 100644
--- a/swo/mpt/cli/core/pricelists/flows.py
+++ b/swo/mpt/cli/core/pricelists/flows.py
@@ -20,17 +20,11 @@
     GENERAL_PRICELIST_ID,
     PRICELIST_ITEMS_ACTION,
     PRICELIST_ITEMS_FIELDS,
-    PRICELIST_ITEMS_ID,
     PRICELIST_ITEMS_ITEM_VENDOR_ID,
     TAB_GENERAL,
     TAB_PRICE_ITEMS,
 )
-from swo.mpt.cli.core.pricelists.to_json import (
-    to_operations_priceitem_json,
-    to_operations_pricelist_json,
-    to_vendor_priceitem_json,
-    to_vendor_pricelist_json,
-)
+from swo.mpt.cli.core.pricelists.models import PriceItemData, PriceListData
 from swo.mpt.cli.core.stats import PricelistStatsCollector
 from swo.mpt.cli.core.utils import (
     add_or_create_error,
@@ -72,33 +66,24 @@ def sync_pricelist(
     file_handler = ExcelFileHandler(file_path)
     with console.status("Sync Pricelist..."):
         general_data = file_handler.get_data_from_vertical_sheet(TAB_GENERAL, GENERAL_FIELDS)
-        if active_account.is_operations():
-            pricelist_json = to_operations_pricelist_json(general_data)
-        else:
-            pricelist_json = to_vendor_pricelist_json(general_data)
-
+        general_data["type"] = active_account.is_operations() and "operations" or "vendor"
+        pricelist_data = PriceListData.from_dict(general_data)
+        pricelist_json = pricelist_data.to_json()
         try:
             if action == PricelistAction.CREATE:
                 pricelist = create_pricelist(mpt_client, pricelist_json)
             else:
-                pricelist_id = general_data[GENERAL_PRICELIST_ID]["value"]
-                pricelist = update_pricelist(mpt_client, pricelist_id, pricelist_json)
+                pricelist = update_pricelist(mpt_client, pricelist_data.id, pricelist_json)
         except Exception as e:
             add_or_create_error(file_handler, TAB_GENERAL, general_data, e)
             stats.add_error(TAB_GENERAL)
             return stats, None
 
-    pricelist_data = general_data[GENERAL_PRICELIST_ID]
-    file_handler.write([{TAB_GENERAL: {pricelist_data["coordinate"]: pricelist.id}}])
+    file_handler.write([{TAB_GENERAL: {pricelist_data.coordinate: pricelist.id}}])
     stats.add_synced(TAB_GENERAL)
 
     stats = sync_pricelist_items(
-        mpt_client,
-        file_path,
-        pricelist.id,
-        active_account,
-        stats,
-        console,
+        mpt_client, file_path, pricelist.id, active_account, stats, console
     )
 
     return stats, pricelist
@@ -118,24 +103,21 @@ def sync_pricelist_items(
         for row in file_handler.get_data_from_horizontal_sheet(sheet_name, PRICELIST_ITEMS_FIELDS):
             try:
                 action = PricelistItemAction(row[PRICELIST_ITEMS_ACTION]["value"])
-
                 if action != PricelistItemAction.UPDATE:
                     stats.add_skipped(sheet_name)
                     continue
 
                 vendor_id = row[PRICELIST_ITEMS_ITEM_VENDOR_ID]["value"]
                 pricelist_item = get_pricelist_item(mpt_client, pricelist_id, vendor_id)
-                if active_account.is_operations():
-                    pricelist_item_json = to_operations_priceitem_json(row)
-                else:
-                    pricelist_item_json = to_vendor_priceitem_json(row)
-
+                row["type"] = active_account.is_operations() and "operations" or "vendor"
+                pricelist_item_data = PriceItemData.from_dict(row)
                 pricelist_item = update_pricelist_item(
-                    mpt_client, pricelist_id, pricelist_item.id, pricelist_item_json
+                    mpt_client, pricelist_id, pricelist_item.id, pricelist_item_data.to_json()
                 )
 
-                index_id = row[PRICELIST_ITEMS_ID]["coordinate"]
-                file_handler.write([{sheet_name: {index_id: pricelist_item.id}}])
+                file_handler.write(
+                    [{sheet_name: {pricelist_item_data.coordinate: pricelist_item.id}}]
+                )
                 stats.add_synced(sheet_name)
             except Exception as e:
                 add_or_create_error(file_handler, sheet_name, row, e)
diff --git a/swo/mpt/cli/core/pricelists/models/__init__.py b/swo/mpt/cli/core/pricelists/models/__init__.py
new file mode 100644
index 00000000..6d1ed3e6
--- /dev/null
+++ b/swo/mpt/cli/core/pricelists/models/__init__.py
@@ -0,0 +1,4 @@
+from .price_item import PriceItemData
+from .price_list import PriceListData
+
+__all__ = ["PriceItemData", "PriceListData"]
diff --git a/swo/mpt/cli/core/pricelists/models/price_item.py b/swo/mpt/cli/core/pricelists/models/price_item.py
new file mode 100644
index 00000000..8ca3f193
--- /dev/null
+++ b/swo/mpt/cli/core/pricelists/models/price_item.py
@@ -0,0 +1,57 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.pricelists import constants
+
+
+@dataclass
+class PriceItemData(BaseDataModel):
+    id: str
+    coordinate: str
+    status: str
+    unit_lp: str
+    unit_pp: str
+    unit_sp: str | None
+    markup: str | None
+    type: str
+
+    def is_operations(self) -> bool:
+        return self.type == "operations"
+
+    def is_vendor(self) -> bool:
+        return self.type == "vendor"
+
+    def is_draft(self) -> bool:
+        return self.status == "Draft"
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "PriceItemData":
+        return cls(
+            id=data[constants.PRICELIST_ITEMS_ID]["value"],
+            coordinate=data[constants.PRICELIST_ITEMS_ID]["coordinate"],
+            status=data[constants.PRICELIST_ITEMS_STATUS]["value"],
+            unit_lp=data[constants.PRICELIST_ITEMS_UNIT_LP]["value"],
+            unit_pp=data[constants.PRICELIST_ITEMS_UNIT_PP]["value"],
+            unit_sp=data.get(constants.PRICELIST_ITEMS_UNIT_SP, {}).get("value"),
+            markup=data.get(constants.PRICELIST_ITEMS_MARKUP, {}).get("value"),
+            type=data["type"],
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        data: dict[str, Any] = {
+            "unitLP": self.unit_lp,
+            "unitPP": self.unit_pp,
+        }
+        if self.is_vendor():
+            data["status"] = self.status
+
+        if self.is_operations():
+            data["markup"] = self.markup
+            if self.unit_sp is not None:
+                data["unitSP"] = self.unit_sp
+
+            if self.status != "Draft":
+                data["status"] = self.status
+
+        return data
diff --git a/swo/mpt/cli/core/pricelists/models/price_list.py b/swo/mpt/cli/core/pricelists/models/price_list.py
new file mode 100644
index 00000000..4845967e
--- /dev/null
+++ b/swo/mpt/cli/core/pricelists/models/price_list.py
@@ -0,0 +1,53 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.pricelists import constants
+
+
+@dataclass
+class PriceListData(BaseDataModel):
+    id: str
+    coordinate: str
+    currency: str
+    precision: int
+    notes: str
+    product_id: int
+    type: str
+    default_markup: str | None = None
+    external_id: str | None = None
+
+    @property
+    def product(self) -> dict[str, Any]:
+        return {"id": self.product_id}
+
+    def is_operations(self) -> bool:
+        return self.type == "operations"
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "PriceListData":
+        return cls(
+            id=data[constants.GENERAL_PRICELIST_ID]["value"],
+            coordinate=data[constants.GENERAL_PRICELIST_ID]["coordinate"],
+            currency=data[constants.GENERAL_CURRENCY]["value"],
+            precision=data[constants.GENERAL_PRECISION]["value"],
+            notes=data[constants.GENERAL_NOTES]["value"],
+            product_id=data[constants.GENERAL_PRODUCT_ID]["value"],
+            type=data["type"],
+            default_markup=data.get(constants.GENERAL_DEFAULT_MARKUP, {}).get("value"),
+            external_id=data.get(constants.EXTERNAL_ID, {}).get("value"),
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        data = {
+            "currency": self.currency,
+            "precision": self.precision,
+            "notes": self.notes,
+            "product": self.product,
+        }
+        if self.is_operations():
+            data["defaultMarkup"] = self.default_markup
+        elif not self.is_operations() and self.external_id:
+            data["externalIds"] = {"vendor": self.external_id}
+
+        return data
diff --git a/swo/mpt/cli/core/pricelists/to_json.py b/swo/mpt/cli/core/pricelists/to_json.py
deleted file mode 100644
index abdab761..00000000
--- a/swo/mpt/cli/core/pricelists/to_json.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from swo.mpt.cli.core.handlers.excel_file_handler import SheetData
-from swo.mpt.cli.core.pricelists import constants
-
-
-def to_operations_pricelist_json(values: SheetData) -> SheetData:
-    pricelist_json = to_vendor_pricelist_json(values)
-    pricelist_json["defaultMarkup"] = values[constants.GENERAL_DEFAULT_MARKUP]["value"]
-
-    return pricelist_json
-
-
-def to_vendor_pricelist_json(values: SheetData) -> SheetData:
-    pricelist_json = {
-        "currency": values[constants.GENERAL_CURRENCY]["value"],
-        "precision": values[constants.GENERAL_PRECISION]["value"],
-        "notes": values[constants.GENERAL_NOTES]["value"],
-        "product": {"id": values[constants.GENERAL_PRODUCT_ID]["value"]},
-    }
-
-    external_id = values.get(constants.EXTERNAL_ID, {}).get("value")
-    if external_id:
-        pricelist_json["externalIds"] = {"vendor": external_id}
-
-    return pricelist_json
-
-
-def to_operations_priceitem_json(values: SheetData) -> SheetData:
-    status = values[constants.PRICELIST_ITEMS_STATUS]["value"]
-    priceitem_json = {
-        "markup": values[constants.PRICELIST_ITEMS_MARKUP]["value"],
-        "unitLP": values[constants.PRICELIST_ITEMS_UNIT_LP]["value"],
-        "unitPP": values[constants.PRICELIST_ITEMS_UNIT_PP]["value"],
-    }
-
-    unit_sp = values.get(constants.PRICELIST_ITEMS_UNIT_SP, {}).get("value")
-    if unit_sp is not None:
-        priceitem_json["unitSP"] = unit_sp
-
-    if status != "Draft":
-        priceitem_json["status"] = status
-
-    return priceitem_json
-
-
-def to_vendor_priceitem_json(values: SheetData) -> SheetData:
-    return {
-        "status": values[constants.PRICELIST_ITEMS_STATUS]["value"],
-        "unitLP": values[constants.PRICELIST_ITEMS_UNIT_LP]["value"],
-        "unitPP": values[constants.PRICELIST_ITEMS_UNIT_PP]["value"],
-    }
diff --git a/swo/mpt/cli/core/products/app.py b/swo/mpt/cli/core/products/app.py
index 634105d9..77814d4a 100644
--- a/swo/mpt/cli/core/products/app.py
+++ b/swo/mpt/cli/core/products/app.py
@@ -144,47 +144,12 @@ def sync_product(
                 status,
             )
 
-        # TODO: call product_stats.to_table()
-        table = _product_stats_table(product_stats)
-        table = _list_products_stats(table, product_stats)
-        console.print(table)
-
+        console.print(product_stats.to_table())
         if product_stats.is_error:
             raise typer.Exit(code=3)
 
 
-# TODO: move to StatsCollector.to_table()
-def _product_stats_table(stats: ProductStatsCollector) -> Table:
-    if stats.is_error:
-        title = "Product Sync [red bold]FAILED"
-    else:
-        title = "Product Sync [green bold]SUCCEED"
-
-    table = Table(title, box=box.ROUNDED)
-
-    table.add_column("Total")
-    table.add_column("Synced")
-    table.add_column("Errors")
-    table.add_column("Skipped")
-
-    return table
-
-
-# TODO: move to StatsCollector.to_table()
-def _list_products_stats(table: Table, stats: ProductStatsCollector) -> Table:
-    for tab_name, tab_stats in stats.tabs.items():
-        table.add_row(
-            tab_name,
-            f"[blue]{tab_stats['total']}",
-            f"[green]{tab_stats['synced']}",
-            f"[red bold]{tab_stats['error']}",
-            f"[white]{tab_stats['skipped']}",
-        )
-
-    return table
-
-
-# TODO: move to StatsCollector.to_table()
+# TODO: move to to_table()
 def _products_table(title: str) -> Table:
     table = Table(title=title, box=box.ROUNDED)
 
@@ -196,7 +161,7 @@ def _products_table(title: str) -> Table:
     return table
 
 
-# TODO: move to StatsCollector.to_table()
+# TODO: move to to_table()
 def _list_products(table: Table, products: list[MPTProduct]) -> Table:
     def _wrap_product_status(status: str) -> str:  # pragma: no cover
         match status:
diff --git a/swo/mpt/cli/core/products/flows.py b/swo/mpt/cli/core/products/flows.py
index c05d967d..9d1f2103 100644
--- a/swo/mpt/cli/core/products/flows.py
+++ b/swo/mpt/cli/core/products/flows.py
@@ -1,6 +1,7 @@
 import enum
 import os
 import re
+from copy import deepcopy
 from functools import partial
 from pathlib import Path
 from typing import Optional, TypeAlias, TypeVar
@@ -19,9 +20,7 @@
     SheetDataGenerator,
 )
 from swo.mpt.cli.core.mpt.client import MPTClient
-from swo.mpt.cli.core.mpt.flows import (
-    create_item as mpt_create_item,
-)
+from swo.mpt.cli.core.mpt.flows import create_item as mpt_create_item
 from swo.mpt.cli.core.mpt.flows import (
     create_item_group,
     create_parameter,
@@ -35,11 +34,8 @@
     search_uom_by_name,
     unpublish_item,
 )
-from swo.mpt.cli.core.mpt.flows import (
-    update_item as mpt_update_item,
-)
+from swo.mpt.cli.core.mpt.flows import update_item as mpt_update_item
 from swo.mpt.cli.core.mpt.models import (
-    Item,
     ItemGroup,
     Parameter,
     ParameterGroup,
@@ -47,19 +43,19 @@
     Template,
 )
 from swo.mpt.cli.core.products import constants
-from swo.mpt.cli.core.products.to_json import (
-    to_item_group_json,
-    to_item_sync_json,
-    to_item_update_or_create_json,
-    to_parameter_group_json,
-    to_parameter_json,
-    to_product_json,
-    to_settings_json,
-    to_template_json,
+from swo.mpt.cli.core.products.models import (
+    ItemData,
+    ItemGroupData,
+    ParameterGroupData,
+    ParametersData,
+    ProductData,
+    TemplateData,
 )
 from swo.mpt.cli.core.stats import ErrorMessagesCollector, ProductStatsCollector
 from swo.mpt.cli.core.utils import (
     add_or_create_error,
+    find_first,
+    find_values_by_pattern,
     set_value,
     status_step_text,
 )
@@ -107,33 +103,31 @@ def check_product_definition(
     """
     Parses Product definition file and check consistency of product definition file
     """
-    # check parameters and items refer to proper groups
     file_handler = ExcelFileHandler(definition_path)
     try:
         file_handler.check_required_sheet(constants.REQUIRED_TABS)
     except RequiredSheetsError as error:
-        for sheet_name in error.details:
-            stats.add_msg(sheet_name, "", "Required tab doesn't exist")
+        for section_name in error.details:
+            stats.add_msg(section_name, "", "Required tab doesn't exist")
 
-    existing_sheets = set(constants.ALL_TABS).intersection(set(file_handler.sheet_names))
-    for sheet_name in existing_sheets:
-        if sheet_name not in constants.REQUIRED_FIELDS_BY_TAB:
+    for section_name in set(constants.ALL_TABS).intersection(set(file_handler.sheet_names)):
+        if section_name not in constants.REQUIRED_FIELDS_BY_TAB:
             continue
 
-        if sheet_name == constants.TAB_GENERAL:
+        if section_name == constants.TAB_GENERAL:
             check_required_general_fields(
                 file_handler,
                 stats,
-                sheet_name,
-                constants.REQUIRED_FIELDS_BY_TAB[sheet_name],
-                constants.REQUIRED_FIELDS_WITH_VALUES_BY_TAB[sheet_name],
+                section_name,
+                constants.REQUIRED_FIELDS_BY_TAB[section_name],
+                constants.REQUIRED_FIELDS_WITH_VALUES_BY_TAB[section_name],
             )
         else:
             check_required_columns(
                 file_handler,
                 stats,
-                sheet_name,
-                constants.REQUIRED_FIELDS_BY_TAB[sheet_name],
+                section_name,
+                constants.REQUIRED_FIELDS_BY_TAB[section_name],
             )
 
     return stats
@@ -142,53 +136,46 @@ def check_product_definition(
 def check_required_general_fields(
     file_handler: ExcelFileHandler,
     stats: ErrorMessagesCollector,
-    sheet_name: str,
+    section_name: str,
     required_field_names: list[str],
     required_values_field_names: list[str],
-) -> ErrorMessagesCollector:
+):
     """
     Check that required fields and values are presented in General worksheet
     """
     try:
-        file_handler.check_required_fields_in_vertical_sheet(sheet_name, required_field_names)
+        file_handler.check_required_fields_in_vertical_sheet(section_name, required_field_names)
     except RequiredFieldsError as error:
         for field in error.details:
-            stats.add_msg(sheet_name, "", f"Required field {field} is not provided")
+            stats.add_msg(section_name, "", f"Required field {field} is not provided")
 
     try:
         file_handler.check_required_field_values_in_vertical_sheet(
-            sheet_name, required_values_field_names
+            section_name, required_values_field_names
         )
     except RequiredFieldValuesError as error:
         for field in error.details:
             stats.add_msg(
-                sheet_name,
+                section_name,
                 field,
                 "Value is not provided for the required field. Current value: None",
             )
 
-    return stats
-
 
 def check_required_columns(
     file_handler: ExcelFileHandler,
     stats: ErrorMessagesCollector,
-    sheet_name: str,
+    section_name: str,
     required_field_names: list[str],
-) -> ErrorMessagesCollector:
+):
     """
     Check that required fields and values are presented in tables worksheet
     """
     try:
-        file_handler.check_required_fields_in_horizontal_sheet(sheet_name, required_field_names)
+        file_handler.check_required_fields_in_horizontal_sheet(section_name, required_field_names)
     except RequiredFieldsError as error:
         for field in error.details:
-            stats.add_msg(
-                sheet_name,
-                "",
-                f"Required field {field} is not provided",
-            )
-    return stats
+            stats.add_msg(section_name, "", f"Required field {field} is not provided")
 
 
 def sync_product_definition(
@@ -203,7 +190,6 @@ def sync_product_definition(
     Sync product definition to the marketplace platform
     """
     file_handler = ExcelFileHandler(file_path=definition_path)
-
     if action == ProductAction.UPDATE:
         stats, product = update_product_definition(
             mpt_client, file_handler, active_account, stats, status
@@ -223,18 +209,21 @@ def create_product_definition(
     general_data = file_handler.get_data_from_vertical_sheet(
         constants.TAB_GENERAL, constants.GENERAL_FIELDS
     )
-    settings_data = file_handler.get_data_from_horizontal_sheet(
-        constants.TAB_SETTINGS, constants.SETTINGS_FIELDS
-    )
+
+    general_data["settings"] = {
+        str(idx): setting for idx, setting in enumerate(
+            file_handler.get_data_from_horizontal_sheet(
+                constants.TAB_SETTINGS, constants.SETTINGS_FIELDS))
+    }
+    product_data = ProductData.from_dict(general_data)
     try:
         product = create_product(
             mpt_client,
-            to_product_json(general_data),
-            to_settings_json(settings_data, constants.SETTINGS_API_MAPPING),
+            product_data.to_json(),
+            product_data.settings.to_json(),
             Path(os.path.dirname(__file__)) / "../icons/fake-icon.png",
         )
-        product_data = general_data[constants.GENERAL_PRODUCT_ID]
-        file_handler.write([{constants.TAB_GENERAL: {product_data["coordinate"]: product.id}}])
+        file_handler.write([{constants.TAB_GENERAL: {product_data.coordinate: product.id}}])
         stats.add_synced(constants.TAB_GENERAL)
 
     except Exception as e:
@@ -242,14 +231,14 @@ def create_product_definition(
         stats.add_error(constants.TAB_GENERAL)
         return stats, None
 
-    parameter_groups = file_handler.get_data_from_horizontal_sheet(
+    parameter_groups_data = file_handler.get_data_from_horizontal_sheet(
         constants.TAB_PARAMETERS_GROUPS, constants.PARAMETERS_GROUPS_FIELDS
     )
     parameters_groups_id_mapping = sync_parameters_groups(
         mpt_client,
         file_handler,
         product,
-        parameter_groups,
+        parameter_groups_data,
         stats,
         status,
     )
@@ -434,8 +423,10 @@ def update_item(
     product_id: str,
     sheet_value: SheetData,
 ) -> None:
-    item_json = to_item_update_or_create_json(product_id, sheet_value, account.is_operations())
-    mpt_update_item(mpt_client, item_id, item_json)
+    data = deepcopy(sheet_value)
+    data["product_id"] = product_id
+    data["is_operations"] = account.is_operations()
+    mpt_update_item(mpt_client, item_id, ItemData.from_dict(data).to_json())
 
 
 def sync_parameters_groups(
@@ -454,16 +445,15 @@ def sync_parameters_groups(
     sheet_name = constants.TAB_PARAMETERS_GROUPS
     for sheet_value in values:
         try:
+            parameter_group_data = ParameterGroupData.from_dict(sheet_value)
             parameter_group = create_parameter_group(
-                mpt_client,
-                product,
-                to_parameter_group_json(sheet_value),
+                mpt_client, product, parameter_group_data.to_json()
             )
-
-            id_data = sheet_value[constants.PARAMETERS_GROUPS_ID]
-            id_mapping[id_data["value"]] = parameter_group
+            id_mapping[parameter_group_data.id] = parameter_group
             # TODO: refactor, should be done out of this function
-            file_handler.write([{sheet_name: {id_data["coordinate"]: parameter_group.id}}])
+            file_handler.write(
+                [{sheet_name: {parameter_group_data.coordinate: parameter_group.id}}]
+            )
             stats.add_synced(sheet_name)
         except Exception as e:
             add_or_create_error(file_handler, sheet_name, sheet_value, e)
@@ -491,16 +481,12 @@ def sync_items_groups(
     sheet_name = constants.TAB_ITEMS_GROUPS
     for sheet_value in values:
         try:
-            item_group = create_item_group(
-                mpt_client,
-                product,
-                to_item_group_json(sheet_value),
-            )
+            item_group_data = ItemGroupData.from_dict(sheet_value)
+            new_item_group = create_item_group(mpt_client, product, item_group_data.to_json())
 
-            id_data = sheet_value[constants.ITEMS_GROUPS_ID]
-            id_mapping[id_data["value"]] = item_group
+            id_mapping[item_group_data.id] = new_item_group
             # TODO: refactor, should be done out of this function
-            file_handler.write([{sheet_name: {id_data["coordinate"]: item_group.id}}])
+            file_handler.write([{sheet_name: {item_group_data.coordinate: new_item_group.id}}])
             stats.add_synced(sheet_name)
         except Exception as e:
             add_or_create_error(file_handler, sheet_name, sheet_value, e)
@@ -529,22 +515,20 @@ def sync_parameters(
     id_mapping = {}
     sheet_name = getattr(constants, f"TAB_{scope.upper()}_PARAMETERS")
     for sheet_value in values:
+        sheet_value["scope"] = scope
+        sheet_value["parameter_groups_mapping"] = {
+            k: ParameterGroupData(**v.model_dump()) for k, v in parameter_groups_mapping.items()
+        }
+        parameter_data = ParametersData.from_dict(sheet_value)
         try:
-            parameter = create_parameter(
-                mpt_client,
-                product,
-                to_parameter_json(scope, parameter_groups_mapping, sheet_value),
-            )
+            new_parameter = create_parameter(mpt_client, product, parameter_data.to_json())
 
-            id_data = sheet_value[constants.ID_COLUMN_NAME]
-            id_mapping[id_data["value"]] = parameter
-
-            fields_to_update = {id_data["coordinate"]: parameter.id}
-            phase = sheet_value[constants.PARAMETERS_PHASE]["value"]
-            if phase == "Order" and scope not in ("Item", "Request"):
-                id_data = sheet_value[constants.PARAMETERS_GROUP_ID]
-                created_group = parameter_groups_mapping[id_data["value"]]
-                fields_to_update[id_data["coordinate"]] = created_group.id
+            id_mapping[parameter_data.id] = new_parameter
+            fields_to_update = {parameter_data.coordinate: new_parameter.id}
+            if parameter_data.is_order_request() and parameter_data.created_group_id is not None:
+                fields_to_update[parameter_data.created_group_coordinate] = (
+                    parameter_data.created_group_id
+                )
 
             # TODO: refactor, should be done out of this function
             file_handler.write([{sheet_name: fields_to_update}])
@@ -574,41 +558,44 @@ def sync_items(
     item_parameters_id_mapping: dict[str, Parameter],
     stats: ProductStatsCollector,
     status: Status,
-) -> SyncResult[Item]:
+) -> None:
     """
     Sync parameters by scope
-    Returns mapping if ids {"<excel-id>": Item}
     """
     id_mapping = {}
     sheet_name = constants.TAB_ITEMS
     for new_sheet_value in values:
-        try:
-            new_sheet_value = setup_unit_of_measure(mpt_client, new_sheet_value)
-            item = mpt_create_item(
-                mpt_client,
-                to_item_sync_json(
-                    product.id,
-                    items_groups_mapping,
-                    item_parameters_id_mapping,
-                    new_sheet_value,
-                ),
+        parameters = []
+        for key, value in find_values_by_pattern(re.compile(r"Parameter\.*"), new_sheet_value):
+            _, external_id = key.split(".")
+            parameter = find_first(
+                lambda p, ext_id=external_id: p.external_id == ext_id,
+                item_parameters_id_mapping.values(),
             )
+            if parameter is None:
+                continue
 
-            id_data = new_sheet_value[constants.ID_COLUMN_NAME]
-            id_mapping[id_data["value"]] = item
+            parameters.append({"id": parameter.id, "value": value["value"]})
 
-            item_id_data = new_sheet_value[constants.ITEMS_UNIT_ID]
-            group_id_data = new_sheet_value[constants.ITEMS_GROUP_ID]
-            created_group = items_groups_mapping[group_id_data["value"]]
+        group_id = new_sheet_value[constants.ITEMS_GROUP_ID]["value"]
+        created_group = items_groups_mapping[group_id]
+        new_sheet_value["group_id"] = created_group.id
+        new_sheet_value["parameters"] = parameters
+        new_sheet_value["product_id"] = product.id
+        try:
+            new_sheet_value = setup_unit_of_measure(mpt_client, new_sheet_value)
+            item_data = ItemData.from_dict(new_sheet_value)
+            item = mpt_create_item(mpt_client, item_data.to_json())
 
+            id_mapping[item_data.id] = item
             # TODO: refactor, should be done out of this function
             file_handler.write(
                 [
                     {
                         sheet_name: {
-                            id_data["coordinate"]: item.id,
-                            item_id_data["coordinate"]: group_id_data["value"],
-                            group_id_data["coordinate"]: created_group.id,
+                            item_data.coordinate: item.id,
+                            item_data.unit_coordinate: item_data.group_id,
+                            item_data.group_coordinate: created_group.id,
                         }
                     }
                 ]
@@ -621,8 +608,6 @@ def sync_items(
             step_text = status_step_text(stats, sheet_name)
             status.update(f"Syncing {sheet_name}: {step_text}")
 
-    return id_mapping
-
 
 def create_item(
     mpt_client: MPTClient,
@@ -633,24 +618,17 @@ def create_item(
     sheet_value: SheetData,
 ) -> None:
     new_sheet_value = setup_unit_of_measure(mpt_client, sheet_value)
-    item = mpt_create_item(
-        mpt_client,
-        to_item_update_or_create_json(
-            product_id,
-            new_sheet_value,
-            active_account.is_operations(),
-        ),
-    )
-
-    id_index = new_sheet_value[constants.ID_COLUMN_NAME]["coordinate"]
-    uom_id_data = new_sheet_value[constants.ITEMS_UNIT_ID]
+    new_sheet_value["product_id"] = product_id
+    new_sheet_value["is_operations"] = active_account.is_operations()
+    item_data = ItemData.from_dict(new_sheet_value)
+    item = mpt_create_item(mpt_client, item_data.to_json())
     # TODO: refactor, should be done out of this function
     file_handler.write(
         [
             {
                 sheet_name: {
-                    id_index: item.id,
-                    uom_id_data["coordinate"]: uom_id_data["value"],
+                    item_data.coordinate: item.id,
+                    item_data.unit_coordinate: item_data.unit_id,
                 }
             }
         ]
@@ -681,18 +659,16 @@ def sync_templates(
     for sheet_value in values:
         try:
             sheet_value = replace_parameter_variables(sheet_value, all_parameters_id_mapping)
-            template = create_template(mpt_client, product, to_template_json(sheet_value))
-
-            content_data = sheet_value[constants.TEMPLATES_CONTENT]
-            id_data = sheet_value[constants.ID_COLUMN_NAME]
-            id_mapping[id_data["value"]] = template
+            template_data = TemplateData.from_dict(sheet_value)
+            template = create_template(mpt_client, product, template_data.to_json())
+            id_mapping[template_data.id] = template
             # TODO: refactor, should be done out of this function
             file_handler.write(
                 [
                     {
                         sheet_name: {
-                            id_data["coordinate"]: template.id,
-                            content_data["coordinate"]: content_data["value"],
+                            template_data.coordinate: template.id,
+                            template_data.content_coordinate: template_data.content,
                         }
                     }
                 ]
diff --git a/swo/mpt/cli/core/products/models/__init__.py b/swo/mpt/cli/core/products/models/__init__.py
new file mode 100644
index 00000000..5e4895a5
--- /dev/null
+++ b/swo/mpt/cli/core/products/models/__init__.py
@@ -0,0 +1,14 @@
+from .items import ItemData, ItemGroupData
+from .parameters import ParameterGroupData, ParametersData
+from .product import ProductData, SettingData
+from .template import TemplateData
+
+__all__ = [
+    "ItemGroupData",
+    "ItemData",
+    "ParameterGroupData",
+    "ParametersData",
+    "ProductData",
+    "SettingData",
+    "TemplateData",
+]
diff --git a/swo/mpt/cli/core/products/models/items.py b/swo/mpt/cli/core/products/models/items.py
new file mode 100644
index 00000000..ae320666
--- /dev/null
+++ b/swo/mpt/cli/core/products/models/items.py
@@ -0,0 +1,127 @@
+from dataclasses import dataclass, field
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.products import constants
+
+
+@dataclass
+class ItemData(BaseDataModel):
+    id: str
+    coordinate: str
+    name: str
+    description: str
+    group_id: str
+    group_coordinate: str
+    product_id: str
+    quantity_not_applicable: bool
+    unit_id: str
+    unit_coordinate: str
+    period: str
+    item_type: str
+    external_ids: str
+    commitment: str
+    parameters: list[dict[str, Any]] = field(default_factory=list)
+
+    @property
+    def group(self) -> dict[str, Any]:
+        return {"id": self.group_id}
+
+    @property
+    def product(self) -> dict[str, Any]:
+        return {"id": self.product_id}
+
+    @property
+    def terms(self) -> dict[str, Any]:
+        data = {"period": self.period}
+        if self.period != "one-time":
+            data["commitment"] = self.commitment
+
+        return data
+
+    @property
+    def unit(self) -> dict[str, Any]:
+        return {"id": self.unit_id}
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "ItemData":
+        if data.get("is_operations", False):
+            item_type = "operations"
+            external_ids = data[constants.ITEMS_ERP_ITEM_ID]["value"]
+        else:
+            item_type = "vendor"
+            external_ids = data[constants.ITEMS_VENDOR_ITEM_ID]["value"]
+
+        try:
+            group_id = data["group_id"]
+        except KeyError:
+            group_id = data[constants.ITEMS_GROUP_ID]["value"]
+
+        return cls(
+            id=data[constants.ITEMS_ID]["value"],
+            coordinate=data[constants.ITEMS_ID]["coordinate"],
+            name=data[constants.ITEMS_NAME]["value"],
+            description=data[constants.ITEMS_DESCRIPTION]["value"],
+            group_id=group_id,
+            group_coordinate=data[constants.ITEMS_GROUP_ID]["coordinate"],
+            product_id=data["product_id"],
+            quantity_not_applicable=data[constants.ITEMS_QUANTITY_APPLICABLE]["value"] == "True",
+            unit_id=data[constants.ITEMS_UNIT_ID]["value"],
+            unit_coordinate=data[constants.ITEMS_UNIT_ID]["coordinate"],
+            parameters=data.get("parameters", []),
+            period=data[constants.ITEMS_BILLING_FREQUENCY]["value"],
+            commitment=data.get(constants.ITEMS_COMMITMENT_TERM, {}).get("value"),
+            item_type=item_type,
+            external_ids=external_ids,
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        return {
+            "name": self.name,
+            "description": self.description,
+            "externalIds": {self.item_type: self.external_ids},
+            "group": self.group,
+            "product": self.product,
+            "quantityNotApplicable": self.quantity_not_applicable,
+            "terms": self.terms,
+            "unit": self.unit,
+            "parameters": self.parameters,
+        }
+
+
+@dataclass
+class ItemGroupData(BaseDataModel):
+    id: str
+    coordinate: str
+    name: str
+    label: str
+    description: str
+    display_order: str
+    default: bool
+    multiple: bool
+    required: bool
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "ItemGroupData":
+        return cls(
+            id=data[constants.ITEMS_GROUPS_ID]["value"],
+            coordinate=data[constants.ITEMS_GROUPS_ID]["coordinate"],
+            name=data[constants.ITEMS_GROUPS_NAME]["value"],
+            label=data[constants.ITEMS_GROUPS_LABEL]["value"],
+            description=data[constants.ITEMS_GROUPS_DESCRIPTION]["value"],
+            display_order=data[constants.ITEMS_GROUPS_DISPLAY_ORDER]["value"],
+            default=data[constants.ITEMS_GROUPS_DEFAULT]["value"] == "True",
+            multiple=data[constants.ITEMS_GROUPS_MULTIPLE_CHOICES]["value"] == "True",
+            required=data[constants.ITEMS_GROUPS_REQUIRED]["value"] == "True",
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        return {
+            "name": self.name,
+            "label": self.label,
+            "description": self.description,
+            "displayOrder": self.display_order,
+            "default": self.default,
+            "multiple": self.multiple,
+            "required": self.required,
+        }
diff --git a/swo/mpt/cli/core/products/models/parameters.py b/swo/mpt/cli/core/products/models/parameters.py
new file mode 100644
index 00000000..3e56cb38
--- /dev/null
+++ b/swo/mpt/cli/core/products/models/parameters.py
@@ -0,0 +1,112 @@
+import json
+from dataclasses import dataclass
+from enum import StrEnum
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.products import constants
+
+
+@dataclass
+class ParameterGroupData(BaseDataModel):
+    id: str
+    name: str
+    coordinate: str | None = None
+    label: str | None = None
+    description: str | None = None
+    displayOrder: str | None = None
+    default: bool | None = None
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "ParameterGroupData":
+        return cls(
+            id=data[constants.PARAMETERS_GROUPS_ID]["value"],
+            coordinate=data[constants.PARAMETERS_GROUPS_ID]["coordinate"],
+            name=data[constants.PARAMETERS_GROUPS_NAME]["value"],
+            label=data[constants.PARAMETERS_GROUPS_LABEL]["value"],
+            description=data[constants.PARAMETERS_GROUPS_DESCRIPTION]["value"],
+            displayOrder=data[constants.PARAMETERS_GROUPS_DISPLAY_ORDER]["value"],
+            default=data[constants.PARAMETERS_GROUPS_DEFAULT]["value"] == "True",
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        return {
+            "name": self.name,
+            "label": self.label,
+            "description": self.description,
+            "displayOrder": self.displayOrder,
+            "default": self.default,
+        }
+
+
+class ScopeEnum(StrEnum):
+    AGREEMENT = "Agreement"
+    ITEM = "Item"
+    REQUEST = "Request"
+    SUBSCRIPTION = "Subscription"
+
+
+@dataclass
+class ParametersData(BaseDataModel):
+    id: str
+    coordinate: str
+    description: str
+    display_order: str
+    external_id: str
+    group_id: str
+    name: str
+    phase: str
+    scope: str
+    type: ScopeEnum
+    constraints: dict[str, Any]
+    options: dict[str, Any]
+    created_group_id: str | None
+    created_group_coordinate: str
+
+    @property
+    def group(self) -> dict[str, Any] | None:
+        if self.is_order_request():
+            return {"id": self.created_group_id}
+
+        return None
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "ParametersData":
+        group_id = data[constants.PARAMETERS_GROUP_ID]["value"]
+        created_group = data["parameter_groups_mapping"].get(group_id) if group_id else None
+        return cls(
+            id=data[constants.PARAMETERS_ID]["value"],
+            coordinate=data[constants.PARAMETERS_ID]["coordinate"],
+            description=data[constants.PARAMETERS_DESCRIPTION]["value"],
+            display_order=data[constants.PARAMETERS_DISPLAY_ORDER]["value"],
+            external_id=data[constants.PARAMETERS_EXTERNALID]["value"],
+            name=data[constants.PARAMETERS_NAME]["value"],
+            phase=data[constants.PARAMETERS_PHASE]["value"],
+            scope=ScopeEnum(data["scope"]),
+            type=data[constants.PARAMETERS_TYPE]["value"],
+            constraints=json.loads(data[constants.PARAMETERS_CONSTRAINTS]["value"]),
+            options=json.loads(data[constants.PARAMETERS_OPTIONS]["value"]),
+            group_id=group_id,
+            created_group_id=created_group.id if created_group else None,
+            created_group_coordinate=data[constants.PARAMETERS_GROUP_ID]["coordinate"],
+        )
+
+    def is_order_request(self) -> bool:
+        return self.phase == "Order" and self.scope not in ("Item", "Request")
+
+    def to_json(self) -> dict[str, Any]:
+        data: dict[str, Any] = {
+            "name": self.name,
+            "description": self.description,
+            "scope": str(self.scope),
+            "phase": self.phase,
+            "type": self.type,
+            "options": {key: value for key, value in self.options.items() if key != "label"},
+            "constraints": self.constraints,
+            "externalId": self.external_id,
+            "displayOrder": self.display_order,
+        }
+        if self.is_order_request():
+            data["group"] = self.group
+
+        return data
diff --git a/swo/mpt/cli/core/products/models/product.py b/swo/mpt/cli/core/products/models/product.py
new file mode 100644
index 00000000..38c3c83b
--- /dev/null
+++ b/swo/mpt/cli/core/products/models/product.py
@@ -0,0 +1,86 @@
+from dataclasses import dataclass, field
+from typing import Any, TypeAlias, TypedDict
+
+from swo.mpt.cli.core.models.data_model import BaseDataModel
+from swo.mpt.cli.core.products import constants
+from swo.mpt.cli.core.utils import set_dict_value
+
+
+class SettingItem(TypedDict):
+    name: str
+    value: str
+    coordinate: str
+
+
+SettingData: TypeAlias = SettingItem
+
+
+@dataclass
+class SettingsData(BaseDataModel):
+    items: list[SettingData] = field(default_factory=list)
+    json_path: str | None = None
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "SettingsData":
+        return cls(
+            items=[
+                SettingItem(
+                    name=item[constants.SETTINGS_SETTING]["value"],
+                    value=item[constants.SETTINGS_VALUE]["value"],
+                    coordinate=item[constants.SETTINGS_VALUE]["coordinate"],
+                )
+                for _, item in data.items()
+            ]
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        settings: dict[str, Any] = {}
+        for setting in self.items:
+            settings_name = setting["name"]
+            settings_value: str | bool = setting["value"]
+            json_path = constants.SETTINGS_API_MAPPING[settings_name]
+
+            # Convert value to boolean only for certain paths
+            if ".label" not in json_path and ".title" not in json_path:
+                settings_value = settings_value == "Enabled"
+
+            settings = set_dict_value(settings, json_path, settings_value)
+
+        return settings
+
+
+@dataclass
+class ProductData(BaseDataModel):
+    id: int
+    coordinate: str
+    name: str
+    short_description: str
+    long_description: str
+    website: int
+    settings: SettingsData
+    external_ids: str | None = None
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "ProductData":
+        settings = (
+            SettingsData.from_dict(data["settings"]) if "settings" in data else SettingsData()
+        )
+        return cls(
+            id=data[constants.GENERAL_PRODUCT_ID]["value"],
+            coordinate=data[constants.GENERAL_PRODUCT_ID]["coordinate"],
+            name=data[constants.GENERAL_PRODUCT_NAME]["value"],
+            short_description=data[constants.GENERAL_CATALOG_DESCRIPTION]["value"],
+            long_description=data[constants.GENERAL_PRODUCT_DESCRIPTION]["value"],
+            website=data[constants.GENERAL_PRODUCT_WEBSITE]["value"],
+            settings=settings
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        return {
+            "name": self.name,
+            "shortDescription": self.short_description,
+            "longDescription": self.long_description,
+            "website": self.website,
+            "externalIds": None,
+            "settings": None,
+        }
diff --git a/swo/mpt/cli/core/products/models/template.py b/swo/mpt/cli/core/products/models/template.py
new file mode 100644
index 00000000..1fd7ed96
--- /dev/null
+++ b/swo/mpt/cli/core/products/models/template.py
@@ -0,0 +1,36 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.products import constants
+
+
+@dataclass
+class TemplateData(BaseDataModel):
+    id: str
+    coordinate: str
+    name: str
+    type: str
+    content: str
+    content_coordinate: str
+    default: bool
+
+    @classmethod
+    def from_dict(cls, data: dict[str, Any]) -> "TemplateData":
+        return cls(
+            id=data[constants.TEMPLATES_ID]["value"],
+            coordinate=data[constants.TEMPLATES_ID]["coordinate"],
+            name=data[constants.TEMPLATES_NAME]["value"],
+            type=data[constants.TEMPLATES_TYPE]["value"],
+            content=data[constants.TEMPLATES_CONTENT]["value"],
+            content_coordinate=data[constants.TEMPLATES_CONTENT]["coordinate"],
+            default=data[constants.TEMPLATES_DEFAULT]["value"] == "True",
+        )
+
+    def to_json(self) -> dict[str, Any]:
+        return {
+            "name": self.name,
+            "type": self.type,
+            "content": self.content,
+            "default": self.default,
+        }
diff --git a/swo/mpt/cli/core/products/to_json.py b/swo/mpt/cli/core/products/to_json.py
deleted file mode 100644
index 379562c8..00000000
--- a/swo/mpt/cli/core/products/to_json.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import json
-import re
-from typing import Any
-
-from swo.mpt.cli.core.handlers.excel_file_handler import SheetData, SheetDataGenerator
-from swo.mpt.cli.core.mpt.models import ItemGroup, Parameter, ParameterGroup
-from swo.mpt.cli.core.products import constants
-from swo.mpt.cli.core.utils import (
-    find_first,
-    find_values_by_pattern,
-    set_dict_value,
-)
-
-
-def to_settings_json(values: SheetDataGenerator, mapping: dict[str, str]) -> dict:
-    settings: dict = {}
-    for value in values:
-        settings_name = value[constants.SETTINGS_SETTING]["value"]
-        settings_value = value[constants.SETTINGS_VALUE]["value"]
-        json_path = mapping[settings_name]
-
-        if ".label" not in json_path and ".title" not in json_path:
-            settings_value = settings_value == "Enabled"
-
-        settings = set_dict_value(settings, json_path, settings_value)
-
-    return settings
-
-
-def to_parameter_group_json(values: SheetData) -> dict[str, Any]:
-    return {
-        "name": values[constants.PARAMETERS_GROUPS_NAME]["value"],
-        "label": values[constants.PARAMETERS_GROUPS_LABEL]["value"],
-        "description": values[constants.PARAMETERS_GROUPS_DESCRIPTION]["value"],
-        "displayOrder": values[constants.PARAMETERS_GROUPS_DISPLAY_ORDER]["value"],
-        "default": values[constants.PARAMETERS_GROUPS_DEFAULT]["value"] == "True",
-    }
-
-
-def to_item_group_json(values: SheetData) -> dict[str, Any]:
-    return {
-        "name": values[constants.ITEMS_GROUPS_NAME]["value"],
-        "label": values[constants.ITEMS_GROUPS_LABEL]["value"],
-        "description": values[constants.ITEMS_GROUPS_DESCRIPTION]["value"],
-        "displayOrder": values[constants.ITEMS_GROUPS_DISPLAY_ORDER]["value"],
-        "default": values[constants.ITEMS_GROUPS_DEFAULT]["value"] == "True",
-        "multiple": values[constants.ITEMS_GROUPS_MULTIPLE_CHOICES]["value"] == "True",
-        "required": values[constants.ITEMS_GROUPS_REQUIRED]["value"] == "True",
-    }
-
-
-def to_parameter_json(
-    scope: str,
-    parameter_group_mapping: dict[str, ParameterGroup],
-    values: SheetData,
-) -> dict[str, Any]:
-    # backward compatible change for V3 Marketplace API
-    options = json.loads(values[constants.PARAMETERS_OPTIONS]["value"])
-    if "label" in options:
-        del options["label"]
-
-    phase = values[constants.PARAMETERS_PHASE]["value"]
-    parameter_json = {
-        "name": values[constants.PARAMETERS_NAME]["value"],
-        "description": values[constants.PARAMETERS_DESCRIPTION]["value"],
-        "scope": scope,
-        "phase": phase,
-        "type": values[constants.PARAMETERS_TYPE]["value"],
-        "options": options,
-        "constraints": json.loads(values[constants.PARAMETERS_CONSTRAINTS]["value"]),
-        "externalId": values[constants.PARAMETERS_EXTERNALID]["value"],
-        "displayOrder": values[constants.PARAMETERS_DISPLAY_ORDER]["value"],
-    }
-
-    if phase == "Order" and scope not in ("Item", "Request"):
-        excel_group_id = values[constants.PARAMETERS_GROUP_ID]["value"]
-        group = parameter_group_mapping[excel_group_id]
-
-        parameter_json["group"] = {"id": group.id}
-
-    return parameter_json
-
-
-def to_product_json(data: SheetData) -> dict[str, Any]:
-    return {
-        "name": data[constants.GENERAL_PRODUCT_NAME]["value"],
-        "shortDescription": data[constants.GENERAL_CATALOG_DESCRIPTION]["value"],
-        "longDescription": data[constants.GENERAL_PRODUCT_DESCRIPTION]["value"],
-        "website": data[constants.GENERAL_PRODUCT_WEBSITE]["value"],
-        "externalIds": None,
-        "settings": None,
-    }
-
-
-def to_item_sync_json(
-    product_id: str,
-    item_group_mapping: dict[str, ItemGroup],
-    item_parameters_id_mapping: dict[str, Parameter],
-    values: SheetData,
-) -> dict[str, Any]:
-    # TODO: remove all precalculation out of this function
-    parameters = []
-    for key, value in find_values_by_pattern(re.compile(r"Parameter\.*"), values):
-        _, external_id = key.split(".")
-        parameter = find_first(
-            lambda p, ext_id=external_id: p.external_id == ext_id,
-            item_parameters_id_mapping.values(),
-        )
-        parameters.append({"id": parameter.id, "value": value["value"]})
-
-    excel_group_id = values[constants.ITEMS_GROUP_ID]["value"]
-    group = item_group_mapping[excel_group_id]
-    return _to_item_json(product_id, group.id, values, parameters)
-
-
-def to_item_update_or_create_json(product_id: str, values: SheetData, is_operations: bool) -> dict:
-    group_id = values[constants.ITEMS_GROUP_ID]["value"]
-    # TODO: Add item parameter update
-    return _to_item_json(product_id, group_id, values, [], is_operations)
-
-
-def _to_item_json(
-    product_id: str,
-    group_id: str,
-    values: SheetData,
-    parameters: list[dict],
-    is_operations: bool = False,
-) -> dict:
-    item_json = {
-        "name": values[constants.ITEMS_NAME]["value"],
-        "description": values[constants.ITEMS_DESCRIPTION]["value"],
-        "group": {
-            "id": group_id,
-        },
-        "product": {
-            "id": product_id,
-        },
-        "quantityNotApplicable": values[constants.ITEMS_QUANTITY_APPLICABLE]["value"] == "True",
-        "unit": {
-            "id": values[constants.ITEMS_UNIT_ID]["value"],
-        },
-        "parameters": parameters,
-    }
-
-    period = values[constants.ITEMS_BILLING_FREQUENCY]["value"]
-    if period == "one-time":
-        item_json["terms"] = {
-            "period": period,
-        }
-    else:
-        item_json["terms"] = {
-            "commitment": values[constants.ITEMS_COMMITMENT_TERM]["value"],
-            "period": period,
-        }
-
-    if is_operations:
-        item_json["externalIds"] = {
-            "operations": values[constants.ITEMS_ERP_ITEM_ID]["value"],
-        }
-    else:
-        item_json["externalIds"] = {
-            "vendor": values[constants.ITEMS_VENDOR_ITEM_ID]["value"],
-        }
-
-    return item_json
-
-
-def to_template_json(values: SheetData) -> dict[str, Any]:
-    return {
-        "name": values[constants.TEMPLATES_NAME]["value"],
-        "type": values[constants.TEMPLATES_TYPE]["value"],
-        "content": values[constants.TEMPLATES_CONTENT]["value"],
-        "default": values[constants.TEMPLATES_DEFAULT]["value"] == "True",
-    }
diff --git a/swo/mpt/cli/core/stats.py b/swo/mpt/cli/core/stats.py
index b3715cf0..33983719 100644
--- a/swo/mpt/cli/core/stats.py
+++ b/swo/mpt/cli/core/stats.py
@@ -1,6 +1,10 @@
 import copy
+from abc import ABC, abstractmethod
 from typing import TypedDict
 
+from rich import box
+from rich.table import Table
+
 
 class Results(TypedDict):
     synced: int
@@ -59,7 +63,9 @@ def __str__(self) -> str:
         return msg
 
 
-class StatsCollector:
+class StatsCollector(ABC):
+    __id: str | None = None
+
     def __init__(self, tabs: dict[str, Results]) -> None:
         self.__has_error = False
         self.__tab_aliases = tabs
@@ -85,6 +91,26 @@ def tabs(self) -> dict[str, Results]:
     def is_error(self) -> bool:
         return self.__has_error
 
+    @abstractmethod
+    def _get_table_title(self) -> str:
+        raise NotImplementedError
+
+    def to_table(self):
+        table = Table(self._get_table_title(), box=box.ROUNDED)
+        columns = ["Total", "Synced", "Errors", "Skipped"]
+        for column in columns:
+            table.add_column(column)
+
+        for tab_name, tab_stats in self.tabs.items():
+            table.add_row(
+                tab_name,
+                f"[blue]{tab_stats['total']}",
+                f"[green]{tab_stats['synced']}",
+                f"[red bold]{tab_stats['error']}",
+                f"[white]{tab_stats['skipped']}",
+            )
+        return table
+
 
 class ProductStatsCollector(StatsCollector):
     def __init__(self) -> None:
@@ -112,6 +138,10 @@ def __init__(self) -> None:
 
         super().__init__(tabs)
 
+    def _get_table_title(self) -> str:
+        status = "[red bold]FAILED" if self.is_error else "[green bold]SUCCEED"
+        return f"Product Sync {status}"
+
 
 class PricelistStatsCollector(StatsCollector):
     def __init__(self) -> None:
@@ -124,3 +154,19 @@ def __init__(self) -> None:
         }
 
         super().__init__(tabs)
+
+    @property
+    def pricelist_id(self) -> str | None:
+        return self.__id
+
+    @pricelist_id.setter
+    def pricelist_id(self, value: str) -> None:
+        self.__id = value
+
+    def _get_table_title(self) -> str:
+        if self.is_error:
+            title = "Pricelist sync [red bold]FAILED"
+        else:
+            title = f"Pricelist {self.pricelist_id} sync [green bold]SUCCEED"
+
+        return title
diff --git a/swo/mpt/cli/core/utils.py b/swo/mpt/cli/core/utils.py
index 9fcce22d..db555f89 100644
--- a/swo/mpt/cli/core/utils.py
+++ b/swo/mpt/cli/core/utils.py
@@ -1,17 +1,18 @@
 import re
-from collections.abc import Generator
+from collections.abc import Generator, Iterable
 from re import Pattern
-from typing import Any
+from typing import Any, Callable, TypeVar
 
 from swo.mpt.cli.core.handlers.excel_file_handler import ExcelFileHandler, SheetData
 from swo.mpt.cli.core.stats import StatsCollector
 
+T = TypeVar("T")
 
-# TODO: add typehints here
-def find_first(func, iterable, default=None):
+def find_first(
+    func: Callable, iterable: Iterable[T], default: T | None = None
+) -> T | None:
     return next(filter(func, iterable), default)
 
-
 def find_values_by_pattern(
     pattern: Pattern[str], values: SheetData
 ) -> Generator[tuple[str, Any], None, None]:
diff --git a/tests/conftest.py b/tests/conftest.py
index 3909d5a8..58d807da 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -373,8 +373,7 @@ def mock_sync_product(
     product,
 ):
     mocker.patch(
-        "swo.mpt.cli.core.products.flows.create_parameter_group",
-        return_value=parameter_group,
+        "swo.mpt.cli.core.products.flows.create_parameter_group", return_value=parameter_group
     )
     mocker.patch("swo.mpt.cli.core.products.flows.create_item_group", return_value=item_group)
     mocker.patch(
diff --git a/tests/test_swo/test_mpt/test_cli/test_core/test_accounts/test_app.py b/tests/test_swo/test_mpt/test_cli/test_core/test_accounts/test_app.py
index 30014b96..db95d6ea 100644
--- a/tests/test_swo/test_mpt/test_cli/test_core/test_accounts/test_app.py
+++ b/tests/test_swo/test_mpt/test_cli/test_core/test_accounts/test_app.py
@@ -44,10 +44,7 @@ def _wrapper(token):
 def test_add_account_accounts_file_not_exists(tmp_path, mocker, new_token):
     account_file_path = tmp_path / ".swocli" / "accounts.json"
     mocker.patch.object(JsonFileHandler, "_default_file_path", account_file_path)
-    mocker.patch(
-        "swo.mpt.cli.core.accounts.app.get_token",
-        return_value=new_token,
-    )
+    mocker.patch("swo.mpt.cli.core.accounts.app.get_token", return_value=new_token)
 
     result = runner.invoke(app, ["add", "new-secret"])
 
@@ -70,10 +67,7 @@ def test_add_account_accounts_file_not_exists(tmp_path, mocker, new_token):
 
 def test_add_account_accounts_file_exists(new_accounts_path, mocker, new_token):
     mocker.patch.object(JsonFileHandler, "_default_file_path", new_accounts_path)
-    mocker.patch(
-        "swo.mpt.cli.core.accounts.app.get_token",
-        return_value=new_token,
-    )
+    mocker.patch("swo.mpt.cli.core.accounts.app.get_token", return_value=new_token)
 
     result = runner.invoke(app, ["add", "new-secret"])
 
@@ -114,10 +108,7 @@ def test_add_account_accounts_file_exists(new_accounts_path, mocker, new_token):
 
 def test_add_account_accounts_override_environment(new_accounts_path, mocker, new_token):
     mocker.patch.object(JsonFileHandler, "_default_file_path", new_accounts_path)
-    mocker.patch(
-        "swo.mpt.cli.core.accounts.app.get_token",
-        return_value=new_token,
-    )
+    mocker.patch("swo.mpt.cli.core.accounts.app.get_token", return_value=new_token)
 
     result = runner.invoke(
         app,
@@ -179,8 +170,7 @@ def test_add_account_token_failed(new_accounts_path, mocker):
 def test_add_existing_account_do_not_replace(new_accounts_path, mocker, existing_token):
     mocker.patch.object(JsonFileHandler, "_default_file_path", new_accounts_path)
     mocker.patch(
-        "swo.mpt.cli.core.accounts.app.get_token",
-        return_value=existing_token("new-super-secret"),
+        "swo.mpt.cli.core.accounts.app.get_token", return_value=existing_token("new-super-secret")
     )
 
     result = runner.invoke(app, ["add", "new-super-secret"], input="N\n")
@@ -214,8 +204,7 @@ def test_add_existing_account_do_not_replace(new_accounts_path, mocker, existing
 def test_add_existing_account_replace(new_accounts_path, mocker, existing_token):
     mocker.patch.object(JsonFileHandler, "_default_file_path", new_accounts_path)
     mocker.patch(
-        "swo.mpt.cli.core.accounts.app.get_token",
-        return_value=existing_token("new-super-secret"),
+        "swo.mpt.cli.core.accounts.app.get_token", return_value=existing_token("new-super-secret")
     )
 
     result = runner.invoke(app, ["add", "new-super-secret"], input="y\n")
diff --git a/tests/test_swo/test_mpt/test_cli/test_core/test_mpt/test_flows.py b/tests/test_swo/test_mpt/test_cli/test_core/test_mpt/test_flows.py
index 2d1629f2..35f977c1 100644
--- a/tests/test_swo/test_mpt/test_cli/test_core/test_mpt/test_flows.py
+++ b/tests/test_swo/test_mpt/test_cli/test_core/test_mpt/test_flows.py
@@ -96,7 +96,7 @@ def test_get_products_with_query(requests_mocker, mpt_client, mpt_products_respo
         json=mpt_products_response,
     )
 
-    meta, products = get_products(mpt_client, 10, 0)
+    _, products = get_products(mpt_client, 10, 0)
 
     assert products == [Product.model_validate(p) for p in mpt_products]
 
@@ -114,18 +114,9 @@ def test_get_products_exception(requests_mocker, mpt_client):
 
 
 def test_create_product(requests_mocker, mpt_client, mpt_product, product_icon_path):
-    requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/products",
-        ),
-        json=mpt_product,
-    )
+    requests_mocker.post(urljoin(mpt_client.base_url, "/catalog/products"), json=mpt_product)
     requests_mocker.put(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{mpt_product['id']}/settings",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{mpt_product['id']}/settings"),
         match=[matchers.json_params_matcher({"settings": "1234"})],
     )
 
@@ -137,13 +128,7 @@ def test_create_product(requests_mocker, mpt_client, mpt_product, product_icon_p
 
 
 def test_create_product_exception(requests_mocker, mpt_client, mpt_product, product_icon_path):
-    requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/products",
-        ),
-        status=500,
-    )
+    requests_mocker.post(urljoin(mpt_client.base_url, "/catalog/products"), status=500)
 
     with pytest.raises(MPTAPIError) as e:
         create_product(
@@ -158,10 +143,7 @@ def test_create_product_exception(requests_mocker, mpt_client, mpt_product, prod
 
 def test_create_parameter_group(requests_mocker, mpt_client, product, mpt_parameter_group):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/parameter-groups",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/parameter-groups"),
         json=mpt_parameter_group,
         match=[matchers.json_params_matcher({"name": "Parameter Group"})],
     )
@@ -173,10 +155,7 @@ def test_create_parameter_group(requests_mocker, mpt_client, product, mpt_parame
 
 def test_create_parameter_group_exception(requests_mocker, mpt_client, product):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/parameter-groups",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/parameter-groups"),
         status=500,
     )
 
@@ -188,10 +167,7 @@ def test_create_parameter_group_exception(requests_mocker, mpt_client, product):
 
 def test_create_item_group(requests_mocker, mpt_client, product, mpt_item_group):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/item-groups",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/item-groups"),
         json=mpt_item_group,
         match=[matchers.json_params_matcher({"name": "Item Group"})],
     )
@@ -203,10 +179,7 @@ def test_create_item_group(requests_mocker, mpt_client, product, mpt_item_group)
 
 def test_create_item_group_exception(requests_mocker, mpt_client, product):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/item-groups",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/item-groups"),
         status=500,
     )
 
@@ -218,10 +191,7 @@ def test_create_item_group_exception(requests_mocker, mpt_client, product):
 
 def test_create_parameter(requests_mocker, mpt_client, product, mpt_parameter):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/parameters",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/parameters"),
         json=mpt_parameter,
         match=[matchers.json_params_matcher({"name": "Parameter Name"})],
     )
@@ -233,11 +203,7 @@ def test_create_parameter(requests_mocker, mpt_client, product, mpt_parameter):
 
 def test_create_parameter_exception(requests_mocker, mpt_client, product):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/parameters",
-        ),
-        status=500,
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/parameters"), status=500
     )
 
     with pytest.raises(MPTAPIError) as e:
@@ -248,10 +214,7 @@ def test_create_parameter_exception(requests_mocker, mpt_client, product):
 
 def test_create_item(requests_mocker, mpt_client, mpt_item):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items",
-        ),
+        urljoin(mpt_client.base_url, "/catalog/items"),
         json=mpt_item,
         match=[matchers.json_params_matcher({"name": "Item Name"})],
     )
@@ -262,13 +225,7 @@ def test_create_item(requests_mocker, mpt_client, mpt_item):
 
 
 def test_create_item_exception(requests_mocker, mpt_client):
-    requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items",
-        ),
-        status=500,
-    )
+    requests_mocker.post(urljoin(mpt_client.base_url, "/catalog/items"), status=500)
 
     with pytest.raises(MPTAPIError) as e:
         create_item(mpt_client, {"name": "Item Name"})
@@ -279,10 +236,7 @@ def test_create_item_exception(requests_mocker, mpt_client):
 def test_search_uom_by_name(requests_mocker, mpt_client, mpt_uom, mpt_uoms_response):
     name = "User"
     requests_mocker.get(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/units-of-measure?name={name}&limit=1&offset=0",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/units-of-measure?name={name}&limit=1&offset=0"),
         json=mpt_uoms_response,
     )
 
@@ -294,10 +248,7 @@ def test_search_uom_by_name(requests_mocker, mpt_client, mpt_uom, mpt_uoms_respo
 def test_search_uom_by_name_exception(requests_mocker, mpt_client):
     name = "User"
     requests_mocker.get(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/units-of-measure?name={name}&limit=1&offset=0",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/units-of-measure?name={name}&limit=1&offset=0"),
         status=500,
     )
 
@@ -310,10 +261,7 @@ def test_search_uom_by_name_exception(requests_mocker, mpt_client):
 def test_search_uom_by_name_not_found(requests_mocker, wrap_to_mpt_list_response, mpt_client):
     name = "User"
     requests_mocker.get(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/units-of-measure?name={name}&limit=1&offset=0",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/units-of-measure?name={name}&limit=1&offset=0"),
         json=wrap_to_mpt_list_response([]),
     )
 
@@ -340,11 +288,7 @@ def test_create_template(requests_mocker, mpt_client, product, mpt_template):
 
 def test_create_template_exception(requests_mocker, product, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/templates",
-        ),
-        status=500,
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/templates"), status=500
     )
 
     with pytest.raises(MPTAPIError) as e:
@@ -355,10 +299,7 @@ def test_create_template_exception(requests_mocker, product, mpt_client):
 
 def test_create_template_400_exception(requests_mocker, product, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            f"/catalog/products/{product.id}/templates",
-        ),
+        urljoin(mpt_client.base_url, f"/catalog/products/{product.id}/templates"),
         status=400,
         json={"errors": {"description": ["error for exception"]}},
     )
@@ -371,11 +312,7 @@ def test_create_template_400_exception(requests_mocker, product, mpt_client):
 
 def test_review_item(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234/review",
-        ),
-        status=200,
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234/review"), status=200
     )
 
     review_item(mpt_client, "ITM-1234-1234")
@@ -383,11 +320,7 @@ def test_review_item(requests_mocker, mpt_client):
 
 def test_review_exception_500(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234/review",
-        ),
-        status=500,
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234/review"), status=500
     )
 
     with pytest.raises(MPTAPIError) as e:
@@ -398,11 +331,7 @@ def test_review_exception_500(requests_mocker, mpt_client):
 
 def test_publish_item(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234/publish",
-        ),
-        status=200,
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234/publish"), status=200
     )
 
     publish_item(mpt_client, "ITM-1234-1234")
@@ -410,11 +339,7 @@ def test_publish_item(requests_mocker, mpt_client):
 
 def test_publish_exception_500(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234/publish",
-        ),
-        status=500,
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234/publish"), status=500
     )
 
     with pytest.raises(MPTAPIError) as e:
@@ -425,11 +350,7 @@ def test_publish_exception_500(requests_mocker, mpt_client):
 
 def test_unpublish_item(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234/unpublish",
-        ),
-        status=200,
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234/unpublish"), status=200
     )
 
     unpublish_item(mpt_client, "ITM-1234-1234")
@@ -437,11 +358,7 @@ def test_unpublish_item(requests_mocker, mpt_client):
 
 def test_unpublish_exception_500(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234/unpublish",
-        ),
-        status=500,
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234/unpublish"), status=500
     )
 
     with pytest.raises(MPTAPIError) as e:
@@ -453,10 +370,7 @@ def test_unpublish_exception_500(requests_mocker, mpt_client):
 def test_update_item(requests_mocker, mpt_client):
     items_json = {"externalIds": {"operations": "erp-id"}}
     requests_mocker.put(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234",
-        ),
+        urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234"),
         status=200,
         match=[matchers.json_params_matcher(items_json)],
     )
@@ -465,13 +379,7 @@ def test_update_item(requests_mocker, mpt_client):
 
 
 def test_update_item_exception_500(requests_mocker, mpt_client):
-    requests_mocker.put(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/items/ITM-1234-1234",
-        ),
-        status=500,
-    )
+    requests_mocker.put(urljoin(mpt_client.base_url, "/catalog/items/ITM-1234-1234"), status=500)
 
     with pytest.raises(MPTAPIError) as e:
         update_item(mpt_client, "ITM-1234-1234", {})
@@ -479,13 +387,7 @@ def test_update_item_exception_500(requests_mocker, mpt_client):
     assert "Internal Server Error" in str(e.value)
 
 
-def test_get_item(
-    requests_mocker,
-    mpt_client,
-    mpt_item,
-    item,
-    wrap_to_mpt_list_response,
-):
+def test_get_item(requests_mocker, mpt_client, mpt_item, item, wrap_to_mpt_list_response):
     requests_mocker.get(
         urljoin(
             mpt_client.base_url,
@@ -514,11 +416,7 @@ def test_get_item_exception(requests_mocker, mpt_client):
     assert "Internal Server Error" in str(e.value)
 
 
-def test_get_item_not_found(
-    requests_mocker,
-    mpt_client,
-    wrap_to_mpt_list_response,
-):
+def test_get_item_not_found(requests_mocker, mpt_client, wrap_to_mpt_list_response):
     requests_mocker.get(
         urljoin(
             mpt_client.base_url,
@@ -535,11 +433,7 @@ def test_get_item_not_found(
 
 def test_get_pricelist(requests_mocker, mpt_client, mpt_pricelist, pricelist):
     requests_mocker.get(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/price-lists/PRC-1234-1234",
-        ),
-        json=mpt_pricelist,
+        urljoin(mpt_client.base_url, "/catalog/price-lists/PRC-1234-1234"), json=mpt_pricelist
     )
 
     returned_pricelist = get_pricelist(mpt_client, "PRC-1234-1234")
@@ -549,11 +443,7 @@ def test_get_pricelist(requests_mocker, mpt_client, mpt_pricelist, pricelist):
 
 def test_get_pricelist_exception(requests_mocker, mpt_client):
     requests_mocker.get(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/price-lists/PRC-1234-1234",
-        ),
-        status=404,
+        urljoin(mpt_client.base_url, "/catalog/price-lists/PRC-1234-1234"), status=404
     )
 
     with pytest.raises(MPTAPIError) as e:
@@ -564,12 +454,9 @@ def test_get_pricelist_exception(requests_mocker, mpt_client):
 
 def test_create_pricelist(requests_mocker, mpt_client, mpt_pricelist, pricelist):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/price-lists",
-        ),
+        urljoin(mpt_client.base_url, "/catalog/price-lists"),
         json=mpt_pricelist,
-        match=[matchers.json_params_matcher({"id": "PRC-1234-1234"})],
+        match=[matchers.json_params_matcher({"id": pricelist.id})],
     )
 
     returned_pricelist = create_pricelist(mpt_client, {"id": "PRC-1234-1234"})
@@ -579,10 +466,7 @@ def test_create_pricelist(requests_mocker, mpt_client, mpt_pricelist, pricelist)
 
 def test_create_pricelist_exception(requests_mocker, mpt_client):
     requests_mocker.post(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/price-lists",
-        ),
+        urljoin(mpt_client.base_url, "/catalog/price-lists"),
         status=500,
         match=[matchers.json_params_matcher({"id": "PRC-1234-1234"})],
     )
@@ -595,10 +479,7 @@ def test_create_pricelist_exception(requests_mocker, mpt_client):
 
 def test_update_pricelist(requests_mocker, mpt_client, mpt_pricelist, pricelist):
     requests_mocker.put(
-        urljoin(
-            mpt_client.base_url,
-            "/catalog/price-lists/PRC-1234-1234",
-        ),
+        urljoin(mpt_client.base_url, "/catalog/price-lists/PRC-1234-1234"),
         json=mpt_pricelist,
         match=[matchers.json_params_matcher({"id": "PRC-1234-1234"})],
     )
diff --git a/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_app.py b/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_app.py
index 16478956..98043087 100644
--- a/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_app.py
+++ b/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_app.py
@@ -17,17 +17,12 @@ def test_sync_pricelists_not_files_found(pricelist_new_file):
 def test_sync_pricelists_particular_file(mocker, pricelist, pricelist_new_file, expected_account):
     stats = PricelistStatsCollector()
     sync_pricelist = mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.sync_pricelist",
-        return_value=(stats, pricelist),
+        "swo.mpt.cli.core.pricelists.app.sync_pricelist", return_value=(stats, pricelist)
     )
     mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.get_active_account",
-        return_value=expected_account,
-    )
-    mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.check_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.app.get_active_account", return_value=expected_account
     )
+    mocker.patch("swo.mpt.cli.core.pricelists.app.check_pricelist", return_value=pricelist)
 
     result = runner.invoke(app, ["sync", str(pricelist_new_file)], input="y\ny\n")
     assert result.exit_code == 0, result.stdout
@@ -39,17 +34,12 @@ def test_sync_pricelists_particular_dicrectory(
 ):
     stats = PricelistStatsCollector()
     sync_pricelist = mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.sync_pricelist",
-        return_value=(stats, pricelist),
-    )
-    mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.get_active_account",
-        return_value=expected_account,
+        "swo.mpt.cli.core.pricelists.app.sync_pricelist", return_value=(stats, pricelist)
     )
     mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.check_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.app.get_active_account", return_value=expected_account
     )
+    mocker.patch("swo.mpt.cli.core.pricelists.app.check_pricelist", return_value=pricelist)
 
     result = runner.invoke(
         app,
@@ -63,17 +53,12 @@ def test_sync_pricelists_particular_dicrectory(
 def test_sync_pricelists_create_file(mocker, pricelist, pricelist_new_file, expected_account):
     stats = PricelistStatsCollector()
     sync_pricelist = mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.sync_pricelist",
-        return_value=(stats, pricelist),
-    )
-    mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.get_active_account",
-        return_value=expected_account,
+        "swo.mpt.cli.core.pricelists.app.sync_pricelist", return_value=(stats, pricelist)
     )
     mocker.patch(
-        "swo.mpt.cli.core.pricelists.app.check_pricelist",
-        return_value=None,
+        "swo.mpt.cli.core.pricelists.app.get_active_account", return_value=expected_account
     )
+    mocker.patch("swo.mpt.cli.core.pricelists.app.check_pricelist", return_value=None)
 
     result = runner.invoke(app, ["sync", str(pricelist_new_file)], input="y\ny\n")
     assert result.exit_code == 0, result.stdout
diff --git a/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_flows.py b/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_flows.py
index aa4273e2..bc87257b 100644
--- a/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_flows.py
+++ b/tests/test_swo/test_mpt/test_cli/test_core/test_pricelists/test_flows.py
@@ -16,8 +16,7 @@
 
 def test_check_pricelist(mocker, mpt_client, pricelist_new_file, pricelist):
     mocked_get_pricelist = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.get_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.flows.get_pricelist", return_value=pricelist
     )
 
     returned_pricelist = check_pricelist(mpt_client, pricelist_new_file)
@@ -44,12 +43,10 @@ def test_sync_pricelist_create_vendor(
     stats = PricelistStatsCollector()
 
     create_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.create_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.flows.create_pricelist", return_value=pricelist
     )
     sync_items_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items",
-        return_value=stats,
+        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items", return_value=stats
     )
 
     returned_stats, returned_pricelist = sync_pricelist(
@@ -62,7 +59,8 @@ def test_sync_pricelist_create_vendor(
     )
 
     assert returned_stats.tabs[constants.TAB_GENERAL]["synced"] == 1
-    # assert wb[constants.TAB_GENERAL]["B3"].value == pricelist.id
+    file_handler = ExcelFileHandler(pricelist_new_file)
+    assert file_handler.get_cell_value_by_coordinate(constants.TAB_GENERAL, "B3") == pricelist.id
     assert returned_pricelist == pricelist
     create_mock.assert_called_once_with(
         mpt_client,
@@ -91,12 +89,10 @@ def test_sync_pricelist_create_operations(
     stats = PricelistStatsCollector()
 
     create_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.create_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.flows.create_pricelist", return_value=pricelist
     )
     sync_items_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items",
-        return_value=stats,
+        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items", return_value=stats
     )
 
     returned_stats, returned_pricelist = sync_pricelist(
@@ -109,7 +105,8 @@ def test_sync_pricelist_create_operations(
     )
 
     assert returned_stats.tabs[constants.TAB_GENERAL]["synced"] == 1
-    # assert wb[constants.TAB_GENERAL]["B3"].value == pricelist.id
+    file_handler = ExcelFileHandler(pricelist_new_file)
+    assert file_handler.get_cell_value_by_coordinate(constants.TAB_GENERAL, "B3") == pricelist.id
     assert returned_pricelist == pricelist
     create_mock.assert_called_once_with(
         mpt_client,
@@ -139,12 +136,10 @@ def test_sync_pricelist_update_vendor(
     stats = PricelistStatsCollector()
 
     update_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.update_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.flows.update_pricelist", return_value=pricelist
     )
     sync_items_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items",
-        return_value=stats,
+        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items", return_value=stats
     )
 
     returned_stats, returned_pricelist = sync_pricelist(
@@ -186,12 +181,10 @@ def test_sync_pricelist_update_operations(
     stats = PricelistStatsCollector()
 
     update_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.update_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.flows.update_pricelist", return_value=pricelist
     )
     sync_items_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items",
-        return_value=stats,
+        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items", return_value=stats
     )
 
     returned_stats, returned_pricelist = sync_pricelist(
@@ -204,7 +197,8 @@ def test_sync_pricelist_update_operations(
     )
 
     assert returned_stats.tabs[constants.TAB_GENERAL]["synced"] == 1
-    # assert wb[constants.TAB_GENERAL]["B3"].value == pricelist.id
+    file_handler = ExcelFileHandler(pricelist_new_file)
+    assert file_handler.get_cell_value_by_coordinate(constants.TAB_GENERAL, "B3") == pricelist.id
     assert returned_pricelist == pricelist
     update_mock.assert_called_once_with(
         mpt_client,
@@ -238,12 +232,10 @@ def test_sync_pricelist_items_vendor(
 ):
     stats = PricelistStatsCollector()
     update_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.update_pricelist_item",
-        return_value=pricelist_item,
+        "swo.mpt.cli.core.pricelists.flows.update_pricelist_item", return_value=pricelist_item
     )
     mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.get_pricelist_item",
-        return_value=pricelist_item,
+        "swo.mpt.cli.core.pricelists.flows.get_pricelist_item", return_value=pricelist_item
     )
 
     returned_stats = sync_pricelist_items(
@@ -279,12 +271,10 @@ def test_sync_pricelist_items_operations(
 ):
     stats = PricelistStatsCollector()
     update_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.update_pricelist_item",
-        return_value=pricelist_item,
+        "swo.mpt.cli.core.pricelists.flows.update_pricelist_item", return_value=pricelist_item
     )
     mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.get_pricelist_item",
-        return_value=pricelist_item,
+        "swo.mpt.cli.core.pricelists.flows.get_pricelist_item", return_value=pricelist_item
     )
 
     returned_stats = sync_pricelist_items(
@@ -335,12 +325,10 @@ def test_sync_pricelist_create_vendor_external_id(
         ]
     )
     create_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.create_pricelist",
-        return_value=pricelist,
+        "swo.mpt.cli.core.pricelists.flows.create_pricelist", return_value=pricelist
     )
     sync_items_mock = mocker.patch(
-        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items",
-        return_value=stats,
+        "swo.mpt.cli.core.pricelists.flows.sync_pricelist_items", return_value=stats
     )
 
     returned_stats, returned_pricelist = sync_pricelist(
diff --git a/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_app.py b/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_app.py
index df27368b..81c8a030 100644
--- a/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_app.py
+++ b/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_app.py
@@ -9,10 +9,7 @@
 def test_list_products(
     expected_account, mocker, requests_mocker, mpt_client, mpt_products_response
 ):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
     requests_mocker.get(
         urljoin(
             mpt_client.base_url,
@@ -31,10 +28,7 @@ def test_list_products(
 def test_list_products_with_query_and_paging(
     expected_account, mocker, requests_mocker, mpt_client, mpt_products_response
 ):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
     requests_mocker.get(
         urljoin(
             mpt_client.base_url,
@@ -54,17 +48,11 @@ def test_list_products_with_query_and_paging(
 
 
 def test_file_with_extension(expected_account, mocker):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
 
 def test_sync_file_doesnt_exist(expected_account, mocker):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
     result = runner.invoke(
         app,
@@ -76,10 +64,7 @@ def test_sync_file_doesnt_exist(expected_account, mocker):
 
 
 def test_sync_with_dry_run_failure(expected_account, mocker, empty_file):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
     result = runner.invoke(
         app,
@@ -91,10 +76,7 @@ def test_sync_with_dry_run_failure(expected_account, mocker, empty_file):
 
 
 def test_sync_with_dry_run(expected_account, mocker, new_product_file):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
     result = runner.invoke(
         app,
@@ -108,14 +90,8 @@ def test_sync_with_dry_run(expected_account, mocker, new_product_file):
 def test_sync_product_update(
     mocker, expected_account, mock_sync_product, new_product_file, product
 ):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.check_product_exists",
-        return_value=product,
-    )
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.check_product_exists", return_value=product)
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
     result = runner.invoke(
         app,
@@ -131,14 +107,8 @@ def test_sync_product_update(
 def test_sync_product_force_create(
     mocker, expected_account, mock_sync_product, new_product_file, product
 ):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.check_product_exists",
-        return_value=product,
-    )
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.check_product_exists", return_value=product)
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
     result = runner.invoke(
         app,
@@ -152,14 +122,8 @@ def test_sync_product_force_create(
 
 
 def test_sync_product_no_product(mocker, expected_account, mock_sync_product, new_product_file):
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.check_product_exists",
-        return_value=None,
-    )
-    mocker.patch(
-        "swo.mpt.cli.core.products.app.get_active_account",
-        return_value=expected_account,
-    )
+    mocker.patch("swo.mpt.cli.core.products.app.check_product_exists", return_value=None)
+    mocker.patch("swo.mpt.cli.core.products.app.get_active_account", return_value=expected_account)
 
     result = runner.invoke(
         app,
diff --git a/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_flows.py b/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_flows.py
index 9751bb64..023ead1d 100644
--- a/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_flows.py
+++ b/tests/test_swo/test_mpt/test_cli/test_core/test_products/test_flows.py
@@ -193,8 +193,7 @@ def test_check_product_definition_not_all_required_settings(
 def test_sync_parameters_groups(mocker, mpt_client, new_product_file, product, parameter_group):
     stats = ProductStatsCollector()
     parameter_group_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.create_parameter_group",
-        return_value=parameter_group,
+        "swo.mpt.cli.core.products.flows.create_parameter_group", return_value=parameter_group
     )
     file_handler = ExcelFileHandler(new_product_file)
     values = file_handler.get_data_from_horizontal_sheet(
@@ -351,10 +350,7 @@ def test_sync_parameters(mocker, mpt_client, new_product_file, product, paramete
     }
     a2_value = file_handler.get_cell_value_by_coordinate(sheet_name, "A2")
     a3_value = file_handler.get_cell_value_by_coordinate(sheet_name, "A3")
-    assert (a2_value, a3_value) == (
-        parameter.id,
-        parameter.id,
-    )
+    assert (a2_value, a3_value) == (parameter.id, parameter.id)
     assert file_handler.get_cell_value_by_coordinate(sheet_name, "H2") == parameter_group.id
     assert parameter_mock.mock_calls[0].args == (
         mpt_client,
@@ -469,7 +465,7 @@ def test_sync_item(
         constants.TAB_ITEMS, constants.ITEMS_FIELDS, [re.compile(r"Parameter\.*")]
     )
 
-    id_mapping = sync_items(
+    sync_items(
         mpt_client,
         file_handler,
         product,
@@ -485,7 +481,6 @@ def test_sync_item(
         console.status(""),
     )
 
-    assert id_mapping == {"ITM-4944-4118-0001": item, "ITM-4944-4118-0002": item}
     assert file_handler.get_cell_value_by_coordinate(constants.TAB_ITEMS, "A2") == item.id
     assert file_handler.get_cell_value_by_coordinate(constants.TAB_ITEMS, "A3") == item.id
     assert file_handler.get_cell_value_by_coordinate(constants.TAB_ITEMS, "J2") == item_group.id
@@ -711,8 +706,7 @@ def test_sync_product(
     active_vendor_account,
 ):
     parameter_group_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.create_parameter_group",
-        return_value=parameter_group,
+        "swo.mpt.cli.core.products.flows.create_parameter_group", return_value=parameter_group
     )
     item_group_mock = mocker.patch(
         "swo.mpt.cli.core.products.flows.create_item_group", return_value=item_group
@@ -800,8 +794,7 @@ def test_sync_product_extra_columns(
     active_vendor_account,
 ):
     parameter_group_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.create_parameter_group",
-        return_value=parameter_group,
+        "swo.mpt.cli.core.products.flows.create_parameter_group", return_value=parameter_group
     )
     item_group_mock = mocker.patch(
         "swo.mpt.cli.core.products.flows.create_item_group", return_value=item_group
@@ -904,30 +897,14 @@ def test_sync_product_update_product(
     uom,
     items,
 ):
-    review_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.review_item",
-        return_value=None,
-    )
-    publish_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.publish_item",
-        return_value=None,
-    )
-    update_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.mpt_update_item",
-        return_value=None,
-    )
+    review_mock = mocker.patch("swo.mpt.cli.core.products.flows.review_item", return_value=None)
+    publish_mock = mocker.patch("swo.mpt.cli.core.products.flows.publish_item", return_value=None)
+    update_mock = mocker.patch("swo.mpt.cli.core.products.flows.mpt_update_item", return_value=None)
     unpublish_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.unpublish_item",
-        return_value=None,
-    )
-    get_item_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.get_item",
-        side_effect=items,
-    )
-    create_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.mpt_create_item",
-        side_effect=items,
+        "swo.mpt.cli.core.products.flows.unpublish_item", return_value=None
     )
+    get_item_mock = mocker.patch("swo.mpt.cli.core.products.flows.get_item", side_effect=items)
+    create_mock = mocker.patch("swo.mpt.cli.core.products.flows.mpt_create_item", side_effect=items)
     mocker.patch("swo.mpt.cli.core.products.flows.search_uom_by_name", return_value=uom)
 
     stats = ProductStatsCollector()
@@ -984,30 +961,14 @@ def test_sync_product_update_product_operations(
     uom,
     items,
 ):
-    review_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.review_item",
-        return_value=None,
-    )
-    publish_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.publish_item",
-        return_value=None,
-    )
-    update_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.mpt_update_item",
-        return_value=None,
-    )
+    review_mock = mocker.patch("swo.mpt.cli.core.products.flows.review_item", return_value=None)
+    publish_mock = mocker.patch("swo.mpt.cli.core.products.flows.publish_item", return_value=None)
+    update_mock = mocker.patch("swo.mpt.cli.core.products.flows.mpt_update_item", return_value=None)
     unpublish_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.unpublish_item",
-        return_value=None,
-    )
-    get_item_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.get_item",
-        side_effect=items,
-    )
-    create_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.mpt_create_item",
-        side_effect=items,
+        "swo.mpt.cli.core.products.flows.unpublish_item", return_value=None
     )
+    get_item_mock = mocker.patch("swo.mpt.cli.core.products.flows.get_item", side_effect=items)
+    create_mock = mocker.patch("swo.mpt.cli.core.products.flows.mpt_create_item", side_effect=items)
     mocker.patch("swo.mpt.cli.core.products.flows.search_uom_by_name", return_value=uom)
 
     stats = ProductStatsCollector()
@@ -1060,8 +1021,7 @@ def test_sync_product_update_product_operations(
 
 def test_check_product_exists(mocker, mpt_client, new_product_file, product):
     get_products_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.get_products",
-        return_value=({}, [product]),
+        "swo.mpt.cli.core.products.flows.get_products", return_value=({}, [product])
     )
 
     checked_product = check_product_exists(mpt_client, new_product_file)
@@ -1072,8 +1032,7 @@ def test_check_product_exists(mocker, mpt_client, new_product_file, product):
 
 def test_check_product_exists_no_product(mocker, mpt_client, new_product_file):
     get_products_mock = mocker.patch(
-        "swo.mpt.cli.core.products.flows.get_products",
-        return_value=({}, []),
+        "swo.mpt.cli.core.products.flows.get_products", return_value=({}, [])
     )
 
     checked_product = check_product_exists(mpt_client, new_product_file)


Reviews:
user: login: d3rky
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T09:44:07Z
user: login: svazquezco
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T10:04:34Z
user: login: svazquezco
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T10:52:52Z
user: login: svazquezco
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T12:05:24Z
user: login: robcsegal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T12:36:40Z
user: login: svazquezco
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T13:34:37Z
user: login: d3rky
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-28T08:17:59Z
user: login: ruben-sebrango
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:31:32Z

Review comments:
diff_hunk: @@ -0,0 +1,57 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.pricelists import constants
+
+
+@dataclass
+class PriceItemData(BaseDataModel):
+    id: str
+    coordinate: str
path: swo/mpt/cli/core/pricelists/models/price_item.py
user: login: d3rky
body: What does it mean this `coordinate`?
created_at: 2025-05-27T09:42:26Z
author_association: COLLABORATOR
diff_hunk: @@ -7,9 +7,8 @@
 from swo.mpt.cli.core.stats import StatsCollector
 
 
-# TODO: add typehints here
path: swo/mpt/cli/core/utils.py
user: login: d3rky
body: TODO was not satisfied here :-(
created_at: 2025-05-27T09:43:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,57 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.pricelists import constants
+
+
+@dataclass
+class PriceItemData(BaseDataModel):
+    id: str
+    coordinate: str
path: swo/mpt/cli/core/pricelists/models/price_item.py
user: login: svazquezco
body: it refers to position of the data in the file. Some examples:

JsonFile: "pricelists/price_item/PRI-9939-6700-0001-0001", ...
ExcelFile: "A1", "B2", ...
TxtFile: "pricelist.txt#[ID:PRI-9939-6700-0001-0001]"

I was considering other names like position or reference. We can definitely rename it if it's not clear enough. WDYT?


created_at: 2025-05-27T10:04:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,9 +7,8 @@
 from swo.mpt.cli.core.stats import StatsCollector
 
 
-# TODO: add typehints here
path: swo/mpt/cli/core/utils.py
user: login: svazquezco
body: Oops! Good catch! I missed that while fighting with mypy 😆. OMW!
created_at: 2025-05-27T10:52:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,9 +7,8 @@
 from swo.mpt.cli.core.stats import StatsCollector
 
 
-# TODO: add typehints here
path: swo/mpt/cli/core/utils.py
user: login: svazquezco
body: Done!
created_at: 2025-05-27T12:05:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.pricelists import constants
+
+
+@dataclass
+class PriceItemData(BaseDataModel):
+    id: str
+    coordinate: str
path: swo/mpt/cli/core/pricelists/models/price_item.py
user: login: robcsegal
body: Is it the cell, cells, or cell range?
created_at: 2025-05-27T12:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+from dataclasses import dataclass
+from typing import Any
+
+from swo.mpt.cli.core.models import BaseDataModel
+from swo.mpt.cli.core.pricelists import constants
+
+
+@dataclass
+class PriceItemData(BaseDataModel):
+    id: str
+    coordinate: str
path: swo/mpt/cli/core/pricelists/models/price_item.py
user: login: svazquezco
body: Good question! For Excel files, It's just a cell, the position of the ID cell since it's the only one we need to handle the data. 
I considered other options like adding the coordinates of each field, but it'd make the things more complex and we don't really need it. 
created_at: 2025-05-27T13:34:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-27T12:08:36Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=softwareone-platform_swo-marketplace-cli&pullRequest=52) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [5 New issues](https://sonarcloud.io/project/issues?id=softwareone-platform_swo-marketplace-cli&pullRequest=52&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=softwareone-platform_swo-marketplace-cli&pullRequest=52&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=softwareone-platform_swo-marketplace-cli&pullRequest=52&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [97.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=softwareone-platform_swo-marketplace-cli&pullRequest=52&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=softwareone-platform_swo-marketplace-cli&pullRequest=52&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=softwareone-platform_swo-marketplace-cli&pullRequest=52)


author_association: NONE