diff --git a/CHANGES b/CHANGES index b295d510..0b6d8413 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,7 @@ -8.1.5 (xxxx-xx-xx) +8.1.5 (2020-07-15) ------------------ - Fixed API extensions endpoints +- Testing: add product change/add price actions 8.1.4 (2020-06-11) diff --git a/setup.py b/setup.py index 025e5bdc..2b518241 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "astunparse==1.6.3", "attrs>=18.2.0", "black==18.9b0", - "isort[pyproject]", + "isort[pyproject]==4.3.21", "PyYAML==3.13", ] @@ -36,7 +36,7 @@ "pytest-cov==2.5.1", "pytest==3.1.3", # Linting - "isort==4.2.15", + "isort==4.3.21", "flake8==3.3.0", "flake8-blind-except==0.1.1", "flake8-debugger==1.4.0", diff --git a/src/commercetools/services/extensions.py b/src/commercetools/services/extensions.py index df54df64..5db3af38 100644 --- a/src/commercetools/services/extensions.py +++ b/src/commercetools/services/extensions.py @@ -43,7 +43,11 @@ def query( def create(self, draft: types.ExtensionDraft) -> types.Extension: return self._client._post( - "extensions", {}, draft, schemas.ExtensionDraftSchema, schemas.ExtensionSchema + "extensions", + {}, + draft, + schemas.ExtensionDraftSchema, + schemas.ExtensionSchema, ) def update_by_id( diff --git a/src/commercetools/testing/products.py b/src/commercetools/testing/products.py index d829590a..206c02bf 100644 --- a/src/commercetools/testing/products.py +++ b/src/commercetools/testing/products.py @@ -1,12 +1,13 @@ import copy import datetime -import typing import uuid +from typing import List, Optional, Union from marshmallow import Schema from marshmallow import fields as schema_fields from commercetools import schemas, types +from commercetools.testing import utils from commercetools.testing.abstract import BaseModel, ServiceBackend from commercetools.testing.utils import ( create_commercetools_response, @@ -22,7 +23,7 @@ class ProductsModel(BaseModel): _unique_values = ["key"] def _create_from_draft( - self, draft: types.ProductDraft, id: typing.Optional[str] = None + self, draft: types.ProductDraft, id: Optional[str] = None ) -> types.Product: object_id = str(uuid.UUID(id) if id is not None else uuid.uuid4()) @@ -63,11 +64,11 @@ def _create_variant_from_draft( self, draft: types.ProductVariantDraft ) -> types.ProductVariant: - assets: typing.Optional[typing.List[types.Asset]] = None + assets: Optional[List[types.Asset]] = None if draft.assets: assets = self._create_assets_from_draft(draft.assets) - prices: typing.Optional[typing.List[types.Price]] = None + prices: Optional[List[types.Price]] = None if draft.prices: prices = self._create_prices_from_draft(draft.prices) @@ -87,11 +88,11 @@ def _create_variant_from_draft( ) def _create_assets_from_draft( - self, drafts: typing.List[types.AssetDraft] - ) -> typing.List[types.Asset]: - assets: typing.List[types.Asset] = [] + self, drafts: List[types.AssetDraft] + ) -> List[types.Asset]: + assets: List[types.Asset] = [] for draft in drafts: - custom: typing.Optional[types.CustomFields] = None + custom: Optional[types.CustomFields] = None if draft.custom: custom = custom_fields_from_draft(self._storage, draft.custom) @@ -107,9 +108,9 @@ def _create_assets_from_draft( return assets def _create_prices_from_draft( - self, drafts: typing.List[types.PriceDraft] - ) -> typing.List[types.Price]: - prices: typing.List[types.Price] = [] + self, drafts: List[types.PriceDraft] + ) -> List[types.Price]: + prices: List[types.Price] = [] for draft in drafts: custom = None if draft.custom: @@ -130,8 +131,8 @@ def _create_prices_from_draft( return prices def _create_price_from_draft( - self, draft: typing.Optional[types.TypedMoneyDraft] - ) -> typing.Optional[types.TypedMoney]: + self, draft: Union[types.Money, Optional[types.TypedMoneyDraft]] + ) -> Optional[types.TypedMoney]: if draft is None: return None @@ -163,9 +164,7 @@ def _get_target_obj(obj: dict, staged: bool): return obj["masterData"]["staged"] -def _get_variant( - data_object: dict, *, id: str = "", sku: str = "" -) -> typing.Optional[dict]: +def _get_variant(data_object: dict, *, id: int = "", sku: str = "") -> Optional[dict]: if not data_object: return None @@ -251,6 +250,7 @@ def updater(self, obj: dict, action: types.ProductPublishAction): # not implemented scopes right now. if new["masterData"].get("staged"): new["masterData"]["current"] = new["masterData"]["staged"] + del new["masterData"]["staged"] new["masterData"]["hasStagedChanges"] = False new["masterData"]["published"] = True return new @@ -258,28 +258,32 @@ def updater(self, obj: dict, action: types.ProductPublishAction): return updater +def convert_draft_price( + price_draft: types.PriceDraft, price_id: str = None +) -> types.Price: + tiers: Optional[List[types.PriceTier]] = None + if price_draft.tiers: + tiers = [utils.create_from_draft(tier) for tier in price_draft.tiers] + return types.Price( + id=price_id or str(uuid.uuid4()), + country=price_draft.country, + channel=price_draft.channel, + value=utils._money_to_typed(price_draft.value), + valid_from=price_draft.valid_from, + valid_until=price_draft.valid_until, + discounted=price_draft.discounted, + custom=utils.create_from_draft(price_draft.custom), + tiers=tiers, + ) + + def _set_product_prices(): def updater(self, obj: dict, action: types.ProductSetPricesAction): new = copy.deepcopy(obj) target_obj = _get_target_obj(new, getattr(action, "staged", True)) prices = [] for price_draft in action.prices: - price = types.Price( - id=str(uuid.uuid4()), - country=price_draft.country, - channel=price_draft.channel, - value=types.TypedMoney( - fraction_digits=2, - cent_amount=price_draft.value.cent_amount, - currency_code=price_draft.value.currency_code, - type=types.MoneyType.CENT_PRECISION, - ), - valid_from=price_draft.valid_from, - valid_until=price_draft.valid_until, - discounted=price_draft.discounted, - custom=price_draft.custom, - tiers=price_draft.tiers, - ) + price = convert_draft_price(price_draft) prices.append(price) schema = schemas.PriceSchema() @@ -293,6 +297,61 @@ def updater(self, obj: dict, action: types.ProductSetPricesAction): return updater +def _change_price(): + def updater(self, obj: dict, action: types.ProductChangePriceAction): + new = copy.deepcopy(obj) + staged = action.staged + if staged is None: + staged = True + target_obj = _get_target_obj(new, staged) + changed_price = convert_draft_price(action.price, action.price_id) + schema = schemas.PriceSchema() + + found_price = True + for variant in get_product_variants(target_obj): + for index, price in enumerate(variant["prices"]): + if price["id"] == action.price_id: + variant["prices"][index] = schema.dump(changed_price) + found_price = True + break + if not found_price: + raise ValueError("Could not find price with id %s" % action.price_id) + if staged: + new["masterData"]["hasStagedChanges"] = True + return new + + return updater + + +def _add_price(): + def updater(self, obj: dict, action: types.ProductAddPriceAction): + new = copy.deepcopy(obj) + staged = action.staged + if staged is None: + staged = True + target_obj = _get_target_obj(new, staged) + new_price = convert_draft_price(action.price) + schema = schemas.PriceSchema() + + found_sku = False + for variant in get_product_variants(target_obj): + if variant["sku"] == action.sku: + if "prices" not in variant: + variant["prices"] = [] + elif not variant["prices"]: + variant["prices"] = [] + variant["prices"].append(schema.dump(new_price)) + found_sku = True + break + if not found_sku: + raise ValueError("Could not find sku %s" % action.sku) + if staged: + new["masterData"]["hasStagedChanges"] = True + return new + + return updater + + class UploadImageQuerySchema(Schema): staged = schema_fields.Bool() filename = schema_fields.Field() @@ -325,6 +384,8 @@ def urls(self): "setAttribute": _set_attribute_action(), "addVariant": _add_variant_action(), "setPrices": _set_product_prices(), + "changePrice": _change_price(), + "addPrice": _add_price(), "publish": _publish_product_action(), } diff --git a/src/commercetools/testing/utils.py b/src/commercetools/testing/utils.py index 3219bf56..3ca14d05 100644 --- a/src/commercetools/testing/utils.py +++ b/src/commercetools/testing/utils.py @@ -51,7 +51,20 @@ def create_from_draft(draft): return None if isinstance(draft, types.CustomFieldsDraft): - return types.CustomFields(type=draft.type, fields=draft.fields) + return types.CustomFields( + type=types.TypeReference(type_id=draft.type.type_id, id=draft.type.id), + fields=draft.fields, + ) + if isinstance(draft, types.PriceTierDraft): + return types.PriceTier( + minimum_quantity=draft.minimum_quantity, + value=_money_to_typed( + types.Money( + cent_amount=draft.value.cent_amount, + currency_code=draft.value.currency_code, + ) + ), + ) raise ValueError(f"Unsupported type {draft.__class__}") diff --git a/tests/test_service_products.py b/tests/test_service_products.py index 5581fa46..ec25cdca 100644 --- a/tests/test_service_products.py +++ b/tests/test_service_products.py @@ -197,3 +197,77 @@ def test_product_update(client): ], ) assert product.key == "test-product" + + +def test_product_update_add_change_price_staged(client): + product = client.products.create( + types.ProductDraft( + key="test-product", + master_variant=types.ProductVariantDraft(sku="1", key="1"), + ) + ) + + product = client.products.update_by_id( + id=product.id, + version=product.version, + actions=[ + types.ProductAddPriceAction( + sku="1", + price=types.PriceDraft( + value=types.Money(cent_amount=1000, currency_code="GBP") + ), + ) + ], + ) + + assert product.master_data.current is None + assert len(product.master_data.staged.master_variant.prices) == 1 + price = product.master_data.staged.master_variant.prices[0] + assert price.value.cent_amount == 1000 + assert price.value.currency_code == "GBP" + + product = client.products.update_by_id( + id=product.id, + version=product.version, + actions=[ + types.ProductChangePriceAction( + price_id=price.id, + price=types.PriceDraft( + value=types.Money(cent_amount=3000, currency_code="EUR") + ), + ) + ], + ) + + assert product.master_data.current is None + assert len(product.master_data.staged.master_variant.prices) == 1 + price = product.master_data.staged.master_variant.prices[0] + assert price.value.cent_amount == 3000 + assert price.value.currency_code == "EUR" + + +def test_product_update_add_price_current(client): + product = client.products.create( + types.ProductDraft( + key="test-product", + master_variant=types.ProductVariantDraft(sku="1", key="1"), + publish=True, + ) + ) + + product = client.products.update_by_id( + id=product.id, + version=product.version, + actions=[ + types.ProductAddPriceAction( + sku="1", + staged=False, + price=types.PriceDraft( + value=types.Money(cent_amount=1000, currency_code="GBP") + ), + ) + ], + ) + + assert product.master_data.staged is None + assert len(product.master_data.current.master_variant.prices) == 1 diff --git a/tox.ini b/tox.ini index f9cb9982..8d256050 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = basepython = python3.7 deps = black==18.9b0 - isort[toml] + isort[toml]==4.3.21 skip_install = true commands = isort --recursive --check-only src tests