From a4ec7373345b1990e7ca38cfa62e86736d60e1af Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 16:30:29 +0300 Subject: [PATCH 01/16] Features: 1) Set `address_line_1` as a required field in the serializer; Fixes: 1) Update Russian locale translation for "order" and "orders"; Extra: 1) Adjust minor formatting in Russian locale files. --- core/crm/amo/gateway.py | 11 ++++++++--- core/locale/ru_RU/LC_MESSAGES/django.mo | Bin 78734 -> 78728 bytes core/locale/ru_RU/LC_MESSAGES/django.po | 2 +- core/models.py | 2 +- core/serializers/utility.py | 2 +- vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo | Bin 11060 -> 11054 bytes vibes_auth/locale/ru_RU/LC_MESSAGES/django.po | 2 +- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index 19ee2585..4fff14a5 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -82,9 +82,14 @@ class AmoCRM: def _get_customer_name(self, order: Order) -> str: if not order.attributes.get("business_identificator"): - return order.user.get_full_name() or ( - f"{order.attributes.get('customer_name')} | " - f"{order.attributes.get('customer_phone_number') or order.attributes.get('customer_email')}" + return ( + order.user.get_full_name() + if order.user + else None + or ( + f"{order.attributes.get('customer_name')} | " + f"{order.attributes.get('customer_phone_number') or order.attributes.get('customer_email')}" + ) ) try: business_identificator = order.attributes.get("business_identificator") diff --git a/core/locale/ru_RU/LC_MESSAGES/django.mo b/core/locale/ru_RU/LC_MESSAGES/django.mo index a4b0facc627473b42114714d07c5c37f59739f50..4bf7a31ff6b2aaeb999d2a815c41bff30d347949 100644 GIT binary patch delta 1952 zcmXZddrX&Q6u|Lw3P=GJ^@oB#E(*xiP;QGx7bQjK7}ktSrp!vTG@3fdXpJ}51!qoj zE$|Y}(R4Itvna>hjJ2pg+)OrGt1T%0XxKsxL<+^|dw$RR*XKFsd4KQwJmGJjZXaq?7j@$7zmf+?|ku`V}+i(zzvGI;8 z?PvHg@gzQD&)s$XV`%TonQ}|I5?2!MKwIiAY{!gybmwfNw%k*c9%467$$@mkt;siG1yQ;FE?@Rc7Dj$+)!D>vrUu7LO z;!gY;mtj_zN)=Y&3wRcpfy@ne`6jeY*YC%{2$ffe_oDr7$hY_bl?vikEa&?&NaGmh z%+Sy=`3_GI7tB;Sh*x|!MY{M8e3ks}DEIs@enngvtb<(uME_7IO?Io8IjJc(!GJ(W!~rbyIa^&$;j6z6aYaYE2l;B~Ygev4MAm;{xl zu>x%XpZIZHqI<3$ZEsw_k1!=kL-$56MiLL8U0l~Z8bvgwuoUx?HMH9s(JFEZtrF*P zAO7a|S1(ril(+>KVcrtANz1X5xCx)fgcOyH=;3pC6>VvXQZ;m(z4vIuk?2D^HUpT4 z>(ks1-@vDc`_TrNvecEd4sC!xqwSSH(T=O8tK?$VzhHziZ;;wX#L$-i&t(1V<|f>bw^x3Q1(A(*);tC delta 1958 zcmXZddrX#f6u|Lwkc$Dy?G=PqMXmyJHJT8M!X=TBnPF)HLuFZ_Ewf>h#bbukHK=SM zW0AA?2d85iU935CGuER1(3xzuW?NYNQ9z;zp+(>GKF?pDbI$L1pXYaf4~+)DJsNy! zq2GxXk=uWYq+-~Z$WqL~MYs-=@nw7w-^GV;22=2Xagju<#T;zL68s1w@h42jQQV1< z6K?xnTt+@HA&#^X{6ZlU>;DqTzys*R6SxA;V=+$S3z&Dq{lF27Bkx7)&=+_Rui|Rl zJSnmPPht~>+~nQ33oCKzrX$itP=3oD@h}#W-@>_=@wZ4U=3_Wkpv_D*+DvUn>+pMM zBR_-IksDq;?zWqkqM!OEoR51j4LckHo2t`TfalPbVjQg_F;gN9xDstCdNCR=U_Snc z*1@OY+^PK( zkCM;gMtkm#+dhr7Lzx5`7zIno+2tcXfMQa{1B7zXM6|mVk@?*${T!NB7#&pDRkhom^MeH3R`eH zeuKGK5UlbH*5XziKt>=5A#UA4+jPTTo)W6^68URrzZ>-|4^vq~-ig(GU#=5$W6@j< z9Fy)RNlfXo(vM%x?bu?MpjYvA4(L>u`C+Ql{Q5Zq4?l&rE6OVD$^9 zp#p79F5?DF&r)f}*U>g#c(#gNt(j=&x(cnM2hrNU#Cp7f4OpC`at1rmPRp8QDs4E3 zwzR8rZH731eFU~UBl0wG18zmTAcpWsd@P^cts>oMKkO?|c?3fXRrcZISc^ZP9pmgG y_k9=82IOC^fq%^%XiI$(EAUsVM-rspr;?401Kp|HhC(l%y0rgN`zK?ED*p#ry*AVU diff --git a/core/locale/ru_RU/LC_MESSAGES/django.po b/core/locale/ru_RU/LC_MESSAGES/django.po index e5bb3e2e..2e934903 100644 --- a/core/locale/ru_RU/LC_MESSAGES/django.po +++ b/core/locale/ru_RU/LC_MESSAGES/django.po @@ -2133,7 +2133,7 @@ msgstr "человекочитаемый идентификатор" #: core/models.py:1261 msgid "order" -msgstr "Заказать" +msgstr "Заказ" #: core/models.py:1282 msgid "a user must have only one pending order at a time" diff --git a/core/models.py b/core/models.py index e5e2303f..8924c846 100644 --- a/core/models.py +++ b/core/models.py @@ -1306,7 +1306,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi def add_product( self, product_uuid=None, - attributes: list | None = None, + attributes: list | dict | None = None, update_quantity=True, ): if attributes is None: diff --git a/core/serializers/utility.py b/core/serializers/utility.py index ff68a104..e7be7054 100644 --- a/core/serializers/utility.py +++ b/core/serializers/utility.py @@ -62,7 +62,7 @@ class AddressCreateSerializer(ModelSerializer): write_only=True, max_length=512, ) - address_line_1 = CharField(write_only=True, max_length=128, required=False) + address_line_1 = CharField(write_only=True, max_length=128, required=True) address_line_2 = CharField(write_only=True, max_length=128, required=False) class Meta: diff --git a/vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo b/vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo index 914d76fb54c764759d16074f3d059bcb18734470..4d3253d2a9ff3ca41e1aecfb1b4c49959ca9b99e 100644 GIT binary patch delta 344 zcmW;H&k8|76vy#jLXonOlq9bIDW%IovXLY#l&mOwOH0L4$<`BC+2|H8pscKU1+tJ= z@EtYHXXebAnRCvgakyA3Gc{}+$Wzm9o^dr F{||=6C$sGj{BAn9m$vd?88kB}`?|Xv7`JD4S&pGEgZ|=gKe;$mFB^7c~9GjR$2N~)!o#P8O@B_=Z z#R4YIq!YA}kb0QHA(}cL?{SGY7|Tnq_<$MQQd$__8eq+ayN_bk7I3ou= z9H4pN7h9O7=n3!8#swzv2TkLL6nF6g&4-@x5|`y`sm)?f;1L^iVGd5wbQoZOU- Date: Wed, 1 Oct 2025 16:47:01 +0300 Subject: [PATCH 02/16] Features: 1) Enhance `__str__` in `payments.models.Transaction` to handle cases where `balance` is missing. Fixes: 1) Adjust feedback creation logic in `core.models` to ensure it is only allowed when `order.status` is `FINISHED`. Extra: 1) Minor formatting adjustment in `payments.models.Transaction` for improved readability. --- core/models.py | 2 +- payments/models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/models.py b/core/models.py index 8924c846..9817d7a3 100644 --- a/core/models.py +++ b/core/models.py @@ -1794,7 +1794,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t if action == "add": if not feedback_exists: - if self.order.status not in ["MOMENTAL", "PENDING", "FAILED"]: + if self.order.status == "FINISHED": return Feedback.objects.create(rating=rating, comment=comment, order_product=self) else: raise ValueError(_("you cannot feedback an order which is not received")) diff --git a/payments/models.py b/payments/models.py index 6678715b..38b4cb16 100644 --- a/payments/models.py +++ b/payments/models.py @@ -22,7 +22,8 @@ class Transaction(NiceModel): process = JSONField(verbose_name=_("processing details"), default=dict) def __str__(self): - return f"{self.balance.user.email} | {self.amount}" + return f"{self.balance.user.email} | {self.amount}" if self.balance else\ + f"{self.order.attributes.get("customer_email")} | {self.amount}" def save(self, **kwargs): if self.amount != 0.0 and ( From 330177f6e486195e508a3726c08df1fe805e6303 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 17:26:07 +0300 Subject: [PATCH 03/16] Features: 1) Add `# noinspection PyUnusedLocal` annotations to various viewsets, filters, and migrations to suppress unnecessary warnings; 2) Improve `post` method in `BusinessPurchaseView` to handle exceptions and inactive orders gracefully; 3) Refactor `resolve_transactions` and related resolvers in Graphene to include more specific typing hints; 4) Include defensive coding for `attributes` in several models to ensure type safety. Fixes: 1) Correct default manager assignment in `Product` model; 2) Address potential division by zero in `AbsoluteFTPStorage`; 3) Ensure proper exception handling for missing `order` attributes in CRM gateway methods; 4) Rectify inaccurate string formatting for `Transaction` `__str__` method. Extra: Refactor various minor code style issues, including formatting corrections in the README, alignment in the emailing utility, and suppressed pycharm-specific inspections; clean up unused imports across files; enhance error messaging consistency. --- README.md | 2 +- core/crm/amo/gateway.py | 7 +++- core/filters.py | 9 +++-- core/graphene/mutations.py | 1 + core/graphene/object_types.py | 14 +++---- core/migrations/0038_backfill_product_sku.py | 2 + core/models.py | 6 ++- core/serializers/detail.py | 2 +- core/utils/emailing.py | 14 ++++++- core/vendors/__init__.py | 2 +- core/views.py | 40 ++++++++++++-------- core/viewsets.py | 17 +++++++++ evibes/ftpstorage.py | 1 + payments/graphene/object_types.py | 3 +- payments/models.py | 7 +++- payments/signals.py | 1 + vibes_auth/viewsets.py | 1 + 17 files changed, 92 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 38a682c7..11ed606f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ before running installment scripts ### nginx Please comment-out SSL-related lines, then apply necessary configurations, run `certbot --cert-only --nginx`, -decomment previously commented lines and enjoy eVibes over HTTPS! +decomment previously commented lines, and enjoy eVibes over HTTPS! ### .env diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index 4fff14a5..a7cbedba 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -81,6 +81,9 @@ class AmoCRM: return payload def _get_customer_name(self, order: Order) -> str: + if type(order.attributes) is not dict: + raise ValueError("order.attributes must be a dict") + if not order.attributes.get("business_identificator"): return ( order.user.get_full_name() @@ -142,6 +145,8 @@ class AmoCRM: body = r.json() return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None) + return None + else: return None @@ -180,5 +185,5 @@ class AmoCRM: if link.order.status == new_status: return - link.order.status = self.STATUS_MAP.get(new_status) + link.order.status = self.STATUS_MAP[new_status] link.order.save(update_fields=["status"]) diff --git a/core/filters.py b/core/filters.py index 3ac968a1..d2a65373 100644 --- a/core/filters.py +++ b/core/filters.py @@ -61,6 +61,7 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter): return qs +# noinspection PyUnusedLocal class ProductFilter(FilterSet): search = CharFilter(field_name="name", method="search_products", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) @@ -153,7 +154,7 @@ class ProductFilter(FilterSet): if not value: return queryset - uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]] + uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]] # type: ignore return queryset.filter(uuid__in=uuids) @@ -396,6 +397,7 @@ class WishlistFilter(FilterSet): fields = ["uuid", "user_email", "user", "order_by"] +# noinspection PyUnusedLocal class CategoryFilter(FilterSet): search = CharFilter(field_name="name", method="search_categories", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") @@ -429,7 +431,7 @@ class CategoryFilter(FilterSet): if not value: return queryset - uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] + uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] # type: ignore return queryset.filter(uuid__in=uuids) @@ -522,6 +524,7 @@ class CategoryFilter(FilterSet): return queryset.filter(parent__uuid=uuid_val) +# noinspection PyUnusedLocal class BrandFilter(FilterSet): search = CharFilter(field_name="name", method="search_brands", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") @@ -547,7 +550,7 @@ class BrandFilter(FilterSet): if not value: return queryset - uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] + uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] # type: ignore return queryset.filter(uuid__in=uuids) diff --git a/core/graphene/mutations.py b/core/graphene/mutations.py index 55583cba..2abe75cf 100644 --- a/core/graphene/mutations.py +++ b/core/graphene/mutations.py @@ -546,6 +546,7 @@ class FeedbackProductAction(BaseMutation): order_product = OrderProduct.objects.get(uuid=order_product_uuid) if user != order_product.order.user: raise PermissionDenied(permission_denied_message) + feedback = None match action: case "add": feedback = order_product.do_feedback(comment=comment, rating=rating, action="add") diff --git a/core/graphene/object_types.py b/core/graphene/object_types.py index 36b57be7..b80fdcc9 100644 --- a/core/graphene/object_types.py +++ b/core/graphene/object_types.py @@ -1,8 +1,9 @@ import logging +from typing import Any from constance import config from django.core.cache import cache -from django.db.models import Max, Min, QuerySet +from django.db.models import Max, Min from django.db.models.functions import Length from django.utils.translation import gettext_lazy as _ from graphene import ( @@ -55,7 +56,6 @@ from core.utils.seo_builders import ( website_schema, ) from payments.graphene.object_types import TransactionType -from payments.models import Transaction logger = logging.getLogger("django") @@ -464,19 +464,19 @@ class OrderType(DjangoObjectType): ) description = _("orders") - def resolve_total_price(self: Order, _info): + def resolve_total_price(self: Order, _info) -> float: return self.total_price - def resolve_total_quantity(self: Order, _info): + def resolve_total_quantity(self: Order, _info) -> int: return self.total_quantity - def resolve_notifications(self: Order, _info): + def resolve_notifications(self: Order, _info) -> dict[str, Any]: return camelize(self.notifications) - def resolve_attributes(self: Order, _info): + def resolve_attributes(self: Order, _info) -> dict[str, Any]: return camelize(self.attributes) - def resolve_payments_transactions(self: Order, _info) -> QuerySet[Transaction] | None: + def resolve_payments_transactions(self: Order, _info): if self.payments_transactions: return self.payments_transactions.all() return None diff --git a/core/migrations/0038_backfill_product_sku.py b/core/migrations/0038_backfill_product_sku.py index 428056bb..b10511d3 100644 --- a/core/migrations/0038_backfill_product_sku.py +++ b/core/migrations/0038_backfill_product_sku.py @@ -9,6 +9,7 @@ def generate_unique_sku(make_candidate, taken): return c +# noinspection PyUnusedLocal def backfill_sku(apps, schema_editor): Product = apps.get_model("core", "Product") from core.utils import generate_human_readable_id as make_candidate @@ -35,6 +36,7 @@ def backfill_sku(apps, schema_editor): last_pk = ids[-1] +# noinspection PyUnusedLocal def noop(apps, schema_editor): pass diff --git a/core/models.py b/core/models.py index 9817d7a3..dd0054e3 100644 --- a/core/models.py +++ b/core/models.py @@ -541,7 +541,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore default=generate_human_readable_id, ) - objects: ProductManager = ProductManager() + objects = ProductManager() class Meta: verbose_name = _("product") @@ -1266,6 +1266,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi @property def is_business(self) -> bool: + if type(self.attributes) is not dict: + self.attributes = {} + self.save() + return False with suppress(Exception): return (self.attributes.get("is_business", False) if self.attributes else False) or ( (self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator")) diff --git a/core/serializers/detail.py b/core/serializers/detail.py index a027ad56..fb8baaca 100644 --- a/core/serializers/detail.py +++ b/core/serializers/detail.py @@ -106,7 +106,7 @@ class CategoryDetailSerializer(ModelSerializer): filterable_results = [] for attr in attributes: - vals = grouped.get(attr.id, []) + vals = grouped.get(attr.id, []) # type: ignore slice_vals = vals[:128] if len(vals) > 128 else vals filterable_results.append( { diff --git a/core/utils/emailing.py b/core/utils/emailing.py index 375b052d..6eb1a50a 100644 --- a/core/utils/emailing.py +++ b/core/utils/emailing.py @@ -51,10 +51,20 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]: except Order.DoesNotExist: return False, f"Order not found with the given pk: {order_pk}" + if type(order.attributes) is not dict: + order.attributes = {} + if not any([order.user, order.attributes.get("email", None), order.attributes.get("customer_email", None)]): return False, f"Order's user not found with the given pk: {order_pk}" - activate(order.user.language) + language = settings.LANGUAGE_CODE + recipient = order.attributes.get("customer_email", "") + + if order.user: + recipient = order.user.email + language = order.user.language + + activate(language) set_email_settings() connection = mail.get_connection() @@ -71,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]: "total_price": order.total_price, }, ), - to=[order.user.email], + to=[recipient], from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", connection=connection, ) diff --git a/core/vendors/__init__.py b/core/vendors/__init__.py index 634369fc..8ec7137d 100644 --- a/core/vendors/__init__.py +++ b/core/vendors/__init__.py @@ -235,7 +235,7 @@ class AbstractVendor: if not rate: raise RatesError(f"No rate found for {currency or self.currency} in {rates} with probider {provider}...") - return float(round(price / rate, 2)) if rate else float(round(price, 2)) + return float(round(price / rate, 2)) if rate else round(price, 2) @staticmethod def round_price_marketologically(price: float) -> float: diff --git a/core/views.py b/core/views.py index 18a9c7af..278c53da 100644 --- a/core/views.py +++ b/core/views.py @@ -428,27 +428,35 @@ class BuyAsBusinessView(APIView): Handles the "POST" request to process a business purchase. """ - @method_decorator(ratelimit(key="ip", rate="2/h", block=True)) + @method_decorator(ratelimit(key="ip", rate="10/h", block=True)) def post(self, request, *_args, **kwargs): serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer.is_valid(raise_exception=True) order = Order.objects.create(status="MOMENTAL") products = [product.get("product_uuid") for product in serializer.validated_data.get("products")] - transaction = order.buy_without_registration( - products=products, - promocode_uuid=serializer.validated_data.get("promocode_uuid"), - customer_name=serializer.validated_data.get("business_identificator"), - customer_email=serializer.validated_data.get("business_email"), - customer_phone_number=serializer.validated_data.get("business_phone_number"), - billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), - shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), - payment_method=serializer.validated_data.get("payment_method"), - is_business=True, - ) - return Response( - status=status.HTTP_201_CREATED, - data=TransactionProcessSerializer(transaction).data, - ) + try: + transaction = order.buy_without_registration( + products=products, + promocode_uuid=serializer.validated_data.get("promocode_uuid"), + customer_name=serializer.validated_data.get("business_identificator"), + customer_email=serializer.validated_data.get("business_email"), + customer_phone_number=serializer.validated_data.get("business_phone_number"), + billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), + shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), + payment_method=serializer.validated_data.get("payment_method"), + is_business=True, + ) + return Response( + status=status.HTTP_201_CREATED, + data=TransactionProcessSerializer(transaction).data, + ) + except Exception as e: + order.is_active = False + order.save() + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={"error": str(e)}, + ) def download_digital_asset_view(request, *args, **kwargs): diff --git a/core/viewsets.py b/core/viewsets.py index 5b3b79b9..4165d148 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -155,6 +155,7 @@ class EvibesViewSet(ModelViewSet): @extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA) +# noinspection PyUnusedLocal class AttributeGroupViewSet(EvibesViewSet): """ Represents a viewset for managing AttributeGroup objects. @@ -187,6 +188,7 @@ class AttributeGroupViewSet(EvibesViewSet): @extend_schema_view(**ATTRIBUTE_SCHEMA) +# noinspection PyUnusedLocal class AttributeViewSet(EvibesViewSet): """ Handles operations related to Attribute objects within the application. @@ -219,6 +221,7 @@ class AttributeViewSet(EvibesViewSet): @extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA) +# noinspection PyUnusedLocal class AttributeValueViewSet(EvibesViewSet): """ A viewset for managing AttributeValue objects. @@ -247,6 +250,7 @@ class AttributeValueViewSet(EvibesViewSet): @extend_schema_view(**CATEGORY_SCHEMA) +# noinspection PyUnusedLocal class CategoryViewSet(EvibesViewSet): """ Manages views for Category-related operations. @@ -377,6 +381,7 @@ class CategoryViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +# noinspection PyUnusedLocal class BrandViewSet(EvibesViewSet): """ Represents a viewset for managing Brand instances. @@ -502,6 +507,7 @@ class BrandViewSet(EvibesViewSet): @extend_schema_view(**PRODUCT_SCHEMA) +# noinspection PyUnusedLocal class ProductViewSet(EvibesViewSet): """ Manages operations related to the `Product` model in the system. @@ -635,6 +641,7 @@ class ProductViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +# noinspection PyUnusedLocal class VendorViewSet(EvibesViewSet): """ Represents a viewset for managing Vendor objects. @@ -666,6 +673,7 @@ class VendorViewSet(EvibesViewSet): @extend_schema_view(**FEEDBACK_SCHEMA) +# noinspection PyUnusedLocal class FeedbackViewSet(EvibesViewSet): """ Representation of a view set handling Feedback objects. @@ -704,6 +712,7 @@ class FeedbackViewSet(EvibesViewSet): @extend_schema_view(**ORDER_SCHEMA) +# noinspection PyUnusedLocal class OrderViewSet(EvibesViewSet): """ ViewSet for managing orders and related operations. @@ -921,6 +930,7 @@ class OrderViewSet(EvibesViewSet): @extend_schema_view(**ORDER_PRODUCT_SCHEMA) +# noinspection PyUnusedLocal class OrderProductViewSet(EvibesViewSet): """ Provides a viewset for managing OrderProduct entities. @@ -993,6 +1003,7 @@ class OrderProductViewSet(EvibesViewSet): return Response(status=status.HTTP_404_NOT_FOUND) +# noinspection PyUnusedLocal class ProductImageViewSet(EvibesViewSet): """ Manages operations related to Product images in the application. @@ -1025,6 +1036,7 @@ class ProductImageViewSet(EvibesViewSet): } +# noinspection PyUnusedLocal class PromoCodeViewSet(EvibesViewSet): """ Manages the retrieval and handling of PromoCode instances through various @@ -1064,6 +1076,7 @@ class PromoCodeViewSet(EvibesViewSet): return qs.filter(user=user) +# noinspection PyUnusedLocal class PromotionViewSet(EvibesViewSet): """ Represents a view set for managing promotions. @@ -1083,6 +1096,7 @@ class PromotionViewSet(EvibesViewSet): } +# noinspection PyUnusedLocal class StockViewSet(EvibesViewSet): """ Handles operations related to Stock data in the system. @@ -1115,6 +1129,7 @@ class StockViewSet(EvibesViewSet): @extend_schema_view(**WISHLIST_SCHEMA) +# noinspection PyUnusedLocal class WishlistViewSet(EvibesViewSet): """ ViewSet for managing Wishlist operations. @@ -1254,6 +1269,7 @@ class WishlistViewSet(EvibesViewSet): @extend_schema_view(**ADDRESS_SCHEMA) +# noinspection PyUnusedLocal class AddressViewSet(EvibesViewSet): """ This class provides viewset functionality for managing `Address` objects. @@ -1329,6 +1345,7 @@ class AddressViewSet(EvibesViewSet): ) +# noinspection PyUnusedLocal class ProductTagViewSet(EvibesViewSet): """ Handles operations related to Product Tags within the application. diff --git a/evibes/ftpstorage.py b/evibes/ftpstorage.py index 7e87e0f6..4e3ebc39 100644 --- a/evibes/ftpstorage.py +++ b/evibes/ftpstorage.py @@ -5,6 +5,7 @@ from storages.backends.ftp import FTPStorage class AbsoluteFTPStorage(FTPStorage): # type: ignore # noinspection PyProtectedMember + # noinspection PyUnresolvedReferences def _get_config(self): cfg = super()._get_config() diff --git a/payments/graphene/object_types.py b/payments/graphene/object_types.py index c39091a3..c704dd80 100644 --- a/payments/graphene/object_types.py +++ b/payments/graphene/object_types.py @@ -1,5 +1,4 @@ import graphene -from django.db.models import QuerySet from graphene import relay from graphene.types.generic import GenericScalar from graphene_django import DjangoObjectType @@ -32,7 +31,7 @@ class BalanceType(DjangoObjectType): interfaces = (relay.Node,) filter_fields = ["is_active"] - def resolve_transaction_set(self: Balance, info) -> QuerySet["Transaction"] | list: + def resolve_transactions(self: Balance, info) -> list: if info.context.user == self.user: # noinspection Mypy return self.transactions.all() or [] diff --git a/payments/models.py b/payments/models.py index 38b4cb16..77d30c70 100644 --- a/payments/models.py +++ b/payments/models.py @@ -22,8 +22,11 @@ class Transaction(NiceModel): process = JSONField(verbose_name=_("processing details"), default=dict) def __str__(self): - return f"{self.balance.user.email} | {self.amount}" if self.balance else\ - f"{self.order.attributes.get("customer_email")} | {self.amount}" + return ( + f"{self.balance.user.email} | {self.amount}" + if self.balance + else f"{self.order.attributes.get('customer_email')} | {self.amount}" + ) def save(self, **kwargs): if self.amount != 0.0 and ( diff --git a/payments/signals.py b/payments/signals.py index 9e9078b1..922de735 100644 --- a/payments/signals.py +++ b/payments/signals.py @@ -22,6 +22,7 @@ def create_balance_on_user_creation_signal(instance, created, **_kwargs): def process_transaction_changes(instance, created, **_kwargs): if created: try: + gateway = None match instance.process.get("gateway", "default"): case "gateway": gateway = AbstractGateway() diff --git a/vibes_auth/viewsets.py b/vibes_auth/viewsets.py index 18373a36..9e756267 100644 --- a/vibes_auth/viewsets.py +++ b/vibes_auth/viewsets.py @@ -30,6 +30,7 @@ from vibes_auth.utils.emailing import send_reset_password_email_task logger = logging.getLogger("django") +# noinspection GrazieInspection @extend_schema_view(**USER_SCHEMA) class UserViewSet( mixins.CreateModelMixin, From b1c022b97419208f980193c0e265684ba5e48d0f Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 17:59:06 +0300 Subject: [PATCH 04/16] Features: 1) Add `promocode_uuid` field to `BuyAsBusinessOrderSerializer`; 2) Enhance error handling in `buy_without_registration` by wrapping in a try-except block and returning appropriate HTTP --- core/models.py | 2 +- core/serializers/utility.py | 1 + core/views.py | 2 +- core/viewsets.py | 25 ++++++++++++++----------- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/core/models.py b/core/models.py index dd0054e3..9d5419de 100644 --- a/core/models.py +++ b/core/models.py @@ -1831,7 +1831,7 @@ class CustomerRelationshipManagementProvider(ExportModelOperationsMixin("crm_pro verbose_name_plural = _("CRMs") -class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): +class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): # type: ignore order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links") crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links") crm_lead_id = CharField(max_length=30, unique=True, db_index=True) diff --git a/core/serializers/utility.py b/core/serializers/utility.py index e7be7054..ec855c61 100644 --- a/core/serializers/utility.py +++ b/core/serializers/utility.py @@ -177,6 +177,7 @@ class BuyUnregisteredOrderSerializer(Serializer): class BuyAsBusinessOrderSerializer(Serializer): products = AddOrderProductSerializer(many=True, required=True) business_identificator = CharField(required=True) + promocode_uuid = UUIDField(required=False) business_email = CharField(required=True) business_phone_number = CharField(required=True) billing_business_address_uuid = CharField(required=False) diff --git a/core/views.py b/core/views.py index 278c53da..3b6bec4c 100644 --- a/core/views.py +++ b/core/views.py @@ -455,7 +455,7 @@ class BuyAsBusinessView(APIView): order.save() return Response( status=status.HTTP_400_BAD_REQUEST, - data={"error": str(e)}, + data={"detail": str(e)}, ) diff --git a/core/viewsets.py b/core/viewsets.py index 4165d148..9b99fe07 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -846,17 +846,20 @@ class OrderViewSet(EvibesViewSet): serializer.is_valid(raise_exception=True) order = Order.objects.create(status="MOMENTAL") products = [p["product_uuid"] for p in serializer.validated_data["products"]] - transaction = order.buy_without_registration( - products=products, - promocode_uuid=serializer.validated_data.get("promocode_uuid"), - customer_name=serializer.validated_data.get("customer_name"), - customer_email=serializer.validated_data.get("customer_email"), - customer_phone_number=serializer.validated_data.get("customer_phone_number"), - billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"), - shipping_customer_address=serializer.validated_data.get("shipping_customer_address_uuid"), - payment_method=serializer.validated_data.get("payment_method"), - ) - return Response(status=status.HTTP_202_ACCEPTED, data=TransactionProcessSerializer(transaction).data) + try: + transaction = order.buy_without_registration( + products=products, + promocode_uuid=serializer.validated_data.get("promocode_uuid"), + customer_name=serializer.validated_data.get("customer_name"), + customer_email=serializer.validated_data.get("customer_email"), + customer_phone_number=serializer.validated_data.get("customer_phone_number"), + billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"), + shipping_customer_address=serializer.validated_data.get("shipping_customer_address_uuid"), + payment_method=serializer.validated_data.get("payment_method"), + ) + return Response(status=status.HTTP_201_CREATED, data=TransactionProcessSerializer(transaction).data) + except Exception as e: + return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)}) @action(detail=True, methods=["post"], url_path="add_order_product") def add_order_product(self, request, **kwargs): From 6a42907060a89502283ff0f5d8b60a2289cad2da Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 18:19:54 +0300 Subject: [PATCH 05/16] Features: 1) Update rate limit logic to dynamically adjust based on `settings.DEBUG` instead of hardcoded `DEBUG`; Fixes: 1) Remove unused import of `DEBUG` from settings; Extra: n/a; --- core/views.py | 2 +- core/viewsets.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/views.py b/core/views.py index 3b6bec4c..5083f47c 100644 --- a/core/views.py +++ b/core/views.py @@ -428,7 +428,7 @@ class BuyAsBusinessView(APIView): Handles the "POST" request to process a business purchase. """ - @method_decorator(ratelimit(key="ip", rate="10/h", block=True)) + @method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h")) def post(self, request, *_args, **kwargs): serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/core/viewsets.py b/core/viewsets.py index 9b99fe07..29169409 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -117,7 +117,6 @@ from core.utils.seo_builders import ( product_schema, website_schema, ) -from evibes.settings import DEBUG from payments.serializers import TransactionProcessSerializer logger = logging.getLogger("django") @@ -840,7 +839,7 @@ class OrderViewSet(EvibesViewSet): return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")}) @action(detail=False, methods=["post"], url_path="buy_unregistered") - @method_decorator(ratelimit(key="ip", rate="5/h" if not DEBUG else "888/h")) + @method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h")) def buy_unregistered(self, request): serializer = BuyUnregisteredOrderSerializer(data=request.data) serializer.is_valid(raise_exception=True) From d2cb4c73f00d3933842c72bb8566cf3027bcede0 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 18:25:31 +0300 Subject: [PATCH 06/16] Features: 1) Allow "retrieve" action in `additional` permissions for AddressViewSet; Fixes: none; Extra: none; --- core/viewsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/viewsets.py b/core/viewsets.py index 29169409..2c2795fb 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -1294,7 +1294,7 @@ class AddressViewSet(EvibesViewSet): filterset_class = AddressFilter queryset = Address.objects.all() serializer_class = AddressSerializer - additional = {"create": "ALLOW"} + additional = {"create": "ALLOW", "retrieve": "ALLOW"} def get_serializer_class(self): if self.action == "create": From c6f20292850dc3c251a4ec2e99fcf41fb0349c8a Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 18:36:00 +0300 Subject: [PATCH 07/16] Features: 1) Add `retrieve` method to handle Address retrieval by UUID; Fixes: None; Extra: None; --- core/viewsets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/viewsets.py b/core/viewsets.py index 2c2795fb..ca6ff0d2 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -1312,6 +1312,13 @@ class AddressViewSet(EvibesViewSet): return Address.objects.none() + def retrieve(self, request, **kwargs): + try: + address = Address.objects.get(uuid=kwargs.get("pk")) + return Response(status=status.HTTP_200_OK, data=self.get_serializer(address).data) + except Address.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + def create(self, request, **kwargs): create_serializer = AddressCreateSerializer(data=request.data, context={"request": request}) create_serializer.is_valid(raise_exception=True) From 4bce9224fce3ded6fe49d86c1e1aa2e72e78d252 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 1 Oct 2025 18:51:11 +0300 Subject: [PATCH 08/16] Features: 1) Automatically create a new Order if no pending Order exists for authenticated users; Fixes: 1) Handle `Order.DoesNotExist` exception in `current` method of viewsets; Extra: 1) Minor improvement to user experience by ensuring an Order object is always returned. --- core/viewsets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/viewsets.py b/core/viewsets.py index ca6ff0d2..78b91faa 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -806,7 +806,10 @@ class OrderViewSet(EvibesViewSet): def current(self, request): if not request.user.is_authenticated: raise PermissionDenied(permission_denied_message) - order = Order.objects.get(user=request.user, status="PENDING") + try: + order = Order.objects.get(user=request.user, status="PENDING") + except Order.DoesNotExist: + order = Order.objects.create(user=request.user) return Response( status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data, From 447f70d17a7e1633e842b29d22750c09f75fdae2 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 13:19:21 +0300 Subject: [PATCH 09/16] Fixes: 1) Handle generic Exception in `current` method of viewsets to prevent 500 errors; Extra: 1) Add fallback error response with status 400 for unexpected exceptions. --- core/viewsets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/viewsets.py b/core/viewsets.py index 78b91faa..ae737b05 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -840,6 +840,8 @@ class OrderViewSet(EvibesViewSet): except Order.DoesNotExist: name = "Order" return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")}) + except Exception as e: + return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)}) @action(detail=False, methods=["post"], url_path="buy_unregistered") @method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h")) From eb69fd3328c4f6efd9695ab78157f6691cf7ee59 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 13:28:35 +0300 Subject: [PATCH 10/16] Fixes: 1) Resolve incorrect URL formatting in AMO CRM gateway by removing redundant "https://" prefix; Extra: None; --- core/crm/amo/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index a7cbedba..7eef103e 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -25,7 +25,7 @@ class AmoCRM: logger.warning("Multiple AMO CRM providers found") raise CRMException("Multiple AMO CRM providers found") from mre - self.base = f"https://{self.instance.integration_url}" + self.base = f"{self.instance.integration_url}" self.client_id = self.instance.authentication.get("client_id") self.client_secret = self.instance.authentication.get("client_secret") From f2456d13919fdb98e16c7175a7422a516ecc2f03 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 13:37:47 +0300 Subject: [PATCH 11/16] Features: 1) Add `is_status_code_success` utility function to check HTTP status success codes; 2) Implement `is_status_code_success` in AMO access token retrieval to improve error handling; Fixes: None; Extra: 1) Adjust minor formatting in `core/utils/__init__.py`; 2) Add logging for failed AMO access token requests; --- core/crm/amo/gateway.py | 5 ++++- core/utils/__init__.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index 7eef103e..ce22c3b7 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -8,6 +8,7 @@ from django.db import transaction from core.crm.exceptions import CRMException from core.models import CustomerRelationshipManagementProvider, Order, OrderCrmLink +from core.utils import is_status_code_success logger = logging.getLogger("django") @@ -59,7 +60,9 @@ class AmoCRM: payload["grant_type"] = "authorization_code" payload["code"] = self.authorization_code r = requests.post(f"{self.base}/oauth2/access_token", json=payload, timeout=15) - r.raise_for_status() + if not is_status_code_success(r.status_code): + logger.error(f"Unable to get AMO access token: {r.status_code} {r.text}") + raise CRMException("Unable to get AMO access token") data = r.json() self.access_token = data["access_token"] cache.set("amo_refresh_token", data["refresh_token"], 604800) diff --git a/core/utils/__init__.py b/core/utils/__init__.py index 53d1f447..6b904606 100644 --- a/core/utils/__init__.py +++ b/core/utils/__init__.py @@ -255,3 +255,7 @@ def generate_human_readable_token() -> str: str: A 20-character random token. """ return "".join([secrets.choice(CROCKFORD) for _ in range(20)]) + + +def is_status_code_success(status_code: int) -> bool: + return 200 <= status_code < 300 From d1ee8d6f97a7d9eab153a9af0423002b5d1077bf Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 13:50:03 +0300 Subject: [PATCH 12/16] Features: 1) Add support for loading `redirect_uri` from dynamic config using `constance.BASE_DOMAIN`; Fixes: 1) Add missing imports for `constance.config` and `django.conf.settings`; Extra: 1) Minor adjustment to `payload` initialization for consistency. --- core/crm/amo/gateway.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index ce22c3b7..49979504 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -3,6 +3,8 @@ import traceback from typing import Optional import requests +from constance import config +from django.conf import settings from django.core.cache import cache from django.db import transaction @@ -52,6 +54,7 @@ class AmoCRM: payload = { "client_id": self.client_id, "client_secret": self.client_secret, + "redirect_uri": f"https://api.{config.BASE_DOMAIN}/", } if self.refresh_token: payload["grant_type"] = "refresh_token" From fa97a582d33013c6db9163f22afd82f37978262e Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 14:26:21 +0300 Subject: [PATCH 13/16] Features: 1) Add fallback logic for `business_identificator` lookup in `order` and `user` attributes; 2) Implement `raise_for_status` for FNS API requests to handle HTTP errors effectively; Fixes: 1) Remove unused Django `settings` import; Extra: 1) Minor reorganization of variable assignments for clarity. --- core/crm/amo/gateway.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index 49979504..b9093e90 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -4,7 +4,6 @@ from typing import Optional import requests from constance import config -from django.conf import settings from django.core.cache import cache from django.db import transaction @@ -90,7 +89,15 @@ class AmoCRM: if type(order.attributes) is not dict: raise ValueError("order.attributes must be a dict") - if not order.attributes.get("business_identificator"): + business_identificator = ( + order.attributes.get("business_identificator") + or order.attributes.get("businessIdentificator") + or order.user.attributes.get("business_identificator") + or order.user.attributes.get("businessIdentificator") + or "" + ) + + if not business_identificator: return ( order.user.get_full_name() if order.user @@ -101,10 +108,10 @@ class AmoCRM: ) ) try: - business_identificator = order.attributes.get("business_identificator") r = requests.get( f"https://api-fns.ru/api/egr?req={business_identificator}&key={self.fns_api_key}", timeout=15 ) + r.raise_for_status() body = r.json() except requests.exceptions.RequestException as rex: logger.error(f"Unable to get company info with FNS: {rex}") From a344014d9f5ad70ce4754e3e3ec873fcfea86480 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 15:08:20 +0300 Subject: [PATCH 14/16] Features: 1) Add annotations for `has_stock`, `has_price`, and `personal_order_only` in product query to enhance filtering and sorting; 2) Implement conditional ordering by `personal_order_only` in product query; Fixes: 1) Remove redundant `personal_order_only` mapping from filters to fix duplicate processing; 2) Adjust sorting logic to avoid appending unneeded `personal_order_only` in filters; Extra: 1) Add missing imports for Django model annotations and fields; 2) Refactor product query for clarity and improved handling of availability and price conditions. --- core/filters.py | 6 +++--- core/graphene/schema.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/core/filters.py b/core/filters.py index d2a65373..98e31eca 100644 --- a/core/filters.py +++ b/core/filters.py @@ -96,7 +96,6 @@ class ProductFilter(FilterSet): ("price_order", "price"), ("sku", "sku"), ("?", "random"), - ("personal_order_only", "personal_order_only"), ), initial="uuid", ) @@ -312,10 +311,11 @@ class ProductFilter(FilterSet): key = "?" mapped_requested.append(key) continue + if key == "personal_order_only": + continue mapped_requested.append(f"-{key}" if desc else key) - has_personal_in_request = any(p.lstrip("-") == "personal_order_only" for p in mapped_requested) - final_ordering = (["personal_order_only"] if not has_personal_in_request else []) + mapped_requested + final_ordering = ["personal_order_only"] + mapped_requested if final_ordering: qs = qs.order_by(*final_ordering) diff --git a/core/graphene/schema.py b/core/graphene/schema.py index 046f695d..5382cb66 100644 --- a/core/graphene/schema.py +++ b/core/graphene/schema.py @@ -1,6 +1,7 @@ import logging from django.core.cache import cache +from django.db.models import Max, Case, When, Value, IntegerField, BooleanField from django.utils import timezone from django.core.exceptions import PermissionDenied from graphene import Field, List, ObjectType, Schema @@ -148,7 +149,7 @@ class Query(ObjectType): product = Product.objects.get(uuid=kwargs["uuid"]) if product.is_active and product.brand.is_active and product.category.is_active: info.context.user.add_to_recently_viewed(product.uuid) - return ( + base_qs = ( Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks") if info.context.user.has_perm("core.view_product") else Product.objects.filter( @@ -162,6 +163,35 @@ class Query(ObjectType): .prefetch_related("images", "stocks") ) + base_qs = ( + base_qs.annotate( + has_stock=Max( + Case( + When(stocks__quantity__gt=0, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ), + has_price=Max( + Case( + When(stocks__price__gt=0, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ), + ) + .annotate( + personal_order_only=Case( + When(has_stock=0, has_price=1, then=Value(True)), + default=Value(False), + output_field=BooleanField(), + ) + ) + .order_by("personal_order_only") + ) + + return base_qs + @staticmethod def resolve_orders(_parent, info, **kwargs): orders = Order.objects From 63b3a939385dbf46e7b8588f97310d10dff313b6 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Thu, 2 Oct 2025 17:13:24 +0300 Subject: [PATCH 15/16] Fixes: 1) Correct logical grouping in `personal_orders_only` property for clarity and accuracy; Extra: None; --- core/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/models.py b/core/models.py index 9d5419de..dcce8b9e 100644 --- a/core/models.py +++ b/core/models.py @@ -599,7 +599,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore @property def personal_orders_only(self) -> bool: - return not self.quantity > 0 and self.price > 0.0 + return not (self.quantity > 0 and self.price > 0.0) class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): # type: ignore [misc, django-manager-missing] From d917584829802d96d98d9414b3e89e45150f8180 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Fri, 3 Oct 2025 16:56:29 +0300 Subject: [PATCH 16/16] Features: 1) Replace IntegerField with BooleanField for `has_stock`, `has_price`, and `personal_orders_only` annotations; 2) Modify final ordering logic to append `personal_orders_only` at the end. Fixes: 1) Handle missing `download` relation in `download_url` method by creating a new `DigitalAssetDownload` instance. Extra: 1) Rename `personal_order_only` to `personal_orders_only` for clarity; 2) Remove unused IntegerField import; 3) General cleanup and minor adjustments in filters logic. --- core/filters.py | 21 ++++++++++----------- core/models.py | 5 ++++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/filters.py b/core/filters.py index 98e31eca..44edfe79 100644 --- a/core/filters.py +++ b/core/filters.py @@ -9,7 +9,6 @@ from django.db.models import ( Case, Exists, FloatField, - IntegerField, Max, OuterRef, Prefetch, @@ -280,21 +279,21 @@ class ProductFilter(FilterSet): qs = qs.annotate( has_stock=Max( Case( - When(stocks__quantity__gt=0, then=Value(1)), - default=Value(0), - output_field=IntegerField(), + When(stocks__quantity__gt=0, then=Value(True)), + default=Value(False), + output_field=BooleanField(), ) ), has_price=Max( Case( - When(stocks__price__gt=0, then=Value(1)), - default=Value(0), - output_field=IntegerField(), + When(stocks__price__gt=0, then=Value(True)), + default=Value(False), + output_field=BooleanField(), ) ), ).annotate( - personal_order_only=Case( - When(has_stock=0, has_price=1, then=Value(True)), + personal_orders_only=Case( + When(has_stock=False, has_price=False, then=Value(True)), default=Value(False), output_field=BooleanField(), ) @@ -311,11 +310,11 @@ class ProductFilter(FilterSet): key = "?" mapped_requested.append(key) continue - if key == "personal_order_only": + if key == "personal_orders_only": continue mapped_requested.append(f"-{key}" if desc else key) - final_ordering = ["personal_order_only"] + mapped_requested + final_ordering = mapped_requested + ["personal_orders_only"] if final_ordering: qs = qs.order_by(*final_ordering) diff --git a/core/models.py b/core/models.py index dcce8b9e..281d484f 100644 --- a/core/models.py +++ b/core/models.py @@ -1780,7 +1780,10 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t def download_url(self: Self) -> str: if self.product and self.product.stocks: if self.product.is_digital and self.product.stocks.first().digital_asset: # type: ignore [union-attr] - return self.download.url + try: + return self.download.url + except self.download.RelatedObjectDoesNotExist: + return DigitalAssetDownload.objects.create(order_product=self).url return "" def do_feedback(self, rating=10, comment="", action="add") -> Optional["Feedback"] | int: