diff --git a/engine/core/fixtures/demo.json b/engine/core/fixtures/demo.json new file mode 100644 index 00000000..e2669881 --- /dev/null +++ b/engine/core/fixtures/demo.json @@ -0,0 +1,710 @@ +{ + "category_tags": [ + {"tag_name": "precious", "name": "Precious Stones", "name_ru": "Драгоценные камни"}, + {"tag_name": "semi-precious", "name": "Semi-Precious Stones", "name_ru": "Полудрагоценные камни"}, + {"tag_name": "organic", "name": "Organic Gems", "name_ru": "Органические камни"} + ], + "product_tags": [ + {"tag_name": "certified", "name": "GIA Certified", "name_ru": "Сертификат GIA"}, + {"tag_name": "ethically-sourced", "name": "Ethically Sourced", "name_ru": "Этичное происхождение"}, + {"tag_name": "rare", "name": "Rare Find", "name_ru": "Редкая находка"}, + {"tag_name": "investment", "name": "Investment Grade", "name_ru": "Инвестиционное качество"}, + {"tag_name": "collector", "name": "Collector's Item", "name_ru": "Коллекционный экземпляр"} + ], + "attribute_groups": [ + {"name": "Physical Properties", "name_ru": "Физические свойства"}, + {"name": "Grading", "name_ru": "Оценка качества"}, + {"name": "Origin", "name_ru": "Происхождение"} + ], + "attributes": [ + {"group": "Physical Properties", "name": "Carat Weight", "name_ru": "Вес в каратах", "value_type": "float", "is_filterable": true}, + {"group": "Physical Properties", "name": "Dimensions (mm)", "name_ru": "Размеры (мм)", "value_type": "string", "is_filterable": false}, + {"group": "Physical Properties", "name": "Cut", "name_ru": "Огранка", "value_type": "string", "is_filterable": true}, + {"group": "Grading", "name": "Color Grade", "name_ru": "Цветовая категория", "value_type": "string", "is_filterable": true}, + {"group": "Grading", "name": "Clarity Grade", "name_ru": "Чистота", "value_type": "string", "is_filterable": true}, + {"group": "Origin", "name": "Country of Origin", "name_ru": "Страна происхождения", "value_type": "string", "is_filterable": true}, + {"group": "Origin", "name": "Mine", "name_ru": "Месторождение", "value_type": "string", "is_filterable": true} + ], + "brands": [ + { + "name": "Sparkle & Stone", + "name_ru": "Искра и Камень", + "description": "Premium gemstone specialists since 1987. Known for exceptional quality and ethical sourcing.", + "description_ru": "Премиальные специалисты по драгоценным камням с 1987 года. Известны исключительным качеством и этичными поставками." + }, + { + "name": "Azure Dreams", + "name_ru": "Лазурные Мечты", + "description": "Specializing in rare blue gemstones from around the world.", + "description_ru": "Специализируемся на редких голубых драгоценных камнях со всего мира." + }, + { + "name": "Crimson Vault", + "name_ru": "Багровое Хранилище", + "description": "Expert purveyors of red and pink precious stones.", + "description_ru": "Эксперты по красным и розовым драгоценным камням." + }, + { + "name": "Evergreen Gems", + "name_ru": "Вечнозелёные Камни", + "description": "The world's finest emeralds and green gemstones.", + "description_ru": "Лучшие изумруды и зелёные драгоценные камни в мире." + }, + { + "name": "Lumina Treasures", + "name_ru": "Сокровища Люмина", + "description": "Collectors' gems with exceptional clarity and fire.", + "description_ru": "Коллекционные камни с исключительной чистотой и игрой света." + }, + { + "name": "Oceanic Pearls", + "name_ru": "Океанический Жемчуг", + "description": "Sustainably harvested pearls from pristine waters.", + "description_ru": "Жемчуг устойчивого происхождения из чистейших вод." + }, + { + "name": "Terra Rara", + "name_ru": "Терра Рара", + "description": "Rare and unusual gemstones for the discerning collector.", + "description_ru": "Редкие и необычные драгоценные камни для взыскательных коллекционеров." + }, + { + "name": "Crystal Kingdom", + "name_ru": "Хрустальное Королевство", + "description": "Quartz varieties and crystal formations of museum quality.", + "description_ru": "Разновидности кварца и кристаллические образования музейного качества." + } + ], + "categories": [ + { + "name": "Gemstones", + "name_ru": "Драгоценные камни", + "description": "Fine gemstones from around the world", + "description_ru": "Изысканные драгоценные камни со всего мира", + "parent": null, + "markup_percent": 0 + }, + { + "name": "Diamonds", + "name_ru": "Бриллианты", + "description": "The hardest natural material on Earth, prized for brilliance and fire", + "description_ru": "Самый твёрдый природный материал на Земле, ценится за блеск и игру света", + "parent": "Gemstones", + "markup_percent": 15 + }, + { + "name": "Rubies", + "name_ru": "Рубины", + "description": "The king of precious stones, known for deep red color", + "description_ru": "Король драгоценных камней, известен глубоким красным цветом", + "parent": "Gemstones", + "markup_percent": 12 + }, + { + "name": "Sapphires", + "name_ru": "Сапфиры", + "description": "Classic blue gemstones with exceptional hardness", + "description_ru": "Классические голубые драгоценные камни с исключительной твёрдостью", + "parent": "Gemstones", + "markup_percent": 10 + }, + { + "name": "Emeralds", + "name_ru": "Изумруды", + "description": "Lush green beryl gemstones, symbol of rebirth", + "description_ru": "Роскошные зелёные камни берилла, символ возрождения", + "parent": "Gemstones", + "markup_percent": 10 + }, + { + "name": "Opals", + "name_ru": "Опалы", + "description": "Play-of-color gemstones with unique patterns", + "description_ru": "Камни с игрой цвета и уникальными узорами", + "parent": "Gemstones", + "markup_percent": 8 + }, + { + "name": "Pearls", + "name_ru": "Жемчуг", + "description": "Organic gems formed within mollusks", + "description_ru": "Органические драгоценности, образующиеся в моллюсках", + "parent": "Gemstones", + "markup_percent": 5 + }, + { + "name": "Amethyst", + "name_ru": "Аметист", + "description": "Purple quartz variety, February birthstone", + "description_ru": "Фиолетовая разновидность кварца, камень рождения февраля", + "parent": "Gemstones", + "markup_percent": 5 + }, + { + "name": "Aquamarine", + "name_ru": "Аквамарин", + "description": "Sea-blue beryl, March birthstone", + "description_ru": "Морской голубой берилл, камень рождения марта", + "parent": "Gemstones", + "markup_percent": 6 + }, + { + "name": "Tanzanite", + "name_ru": "Танзанит", + "description": "Rare blue-violet zoisite from Tanzania", + "description_ru": "Редкий сине-фиолетовый цоизит из Танзании", + "parent": "Gemstones", + "markup_percent": 12 + }, + { + "name": "Tourmaline", + "name_ru": "Турмалин", + "description": "Multi-colored gemstones with electric properties", + "description_ru": "Многоцветные драгоценные камни с электрическими свойствами", + "parent": "Gemstones", + "markup_percent": 7 + } + ], + "products": [ + { + "name": "Round Brilliant Diamond 1.5ct D VVS1", + "name_ru": "Бриллиант круглой огранки 1.5 карата D VVS1", + "description": "Exceptional 1.5 carat round brilliant cut diamond with D color and VVS1 clarity. Triple excellent cut grade with strong blue fluorescence. GIA certified. Perfect for an engagement ring centerpiece.", + "description_ru": "Исключительный бриллиант круглой огранки 1.5 карата с цветом D и чистотой VVS1. Тройная превосходная огранка с сильной голубой флуоресценцией. Сертификат GIA. Идеален для центрального камня обручального кольца.", + "category": "Diamonds", + "brand": "Sparkle & Stone", + "partnumber": "DIA-RB-150-D-VVS1", + "price": 18500, + "purchase_price": 15000, + "quantity": 3 + }, + { + "name": "Princess Cut Diamond 2.0ct E VS2", + "name_ru": "Бриллиант огранки «Принцесса» 2.0 карата E VS2", + "description": "Stunning 2.0 carat princess cut diamond with E color and VS2 clarity. Modern cut with excellent symmetry. GIA certified with laser inscription.", + "description_ru": "Потрясающий бриллиант огранки «Принцесса» 2.0 карата с цветом E и чистотой VS2. Современная огранка с отличной симметрией. Сертификат GIA с лазерной гравировкой.", + "category": "Diamonds", + "brand": "Sparkle & Stone", + "partnumber": "DIA-PC-200-E-VS2", + "price": 24000, + "purchase_price": 19500, + "quantity": 2 + }, + { + "name": "Oval Diamond 1.2ct F IF", + "name_ru": "Бриллиант овальной огранки 1.2 карата F IF", + "description": "Magnificent 1.2 carat oval cut diamond with F color and Internally Flawless clarity. Exceptional fire and brilliance with elongated shape.", + "description_ru": "Великолепный бриллиант овальной огранки 1.2 карата с цветом F и безупречной внутренней чистотой. Исключительная игра света и блеск с удлинённой формой.", + "category": "Diamonds", + "brand": "Lumina Treasures", + "partnumber": "DIA-OV-120-F-IF", + "price": 28500, + "purchase_price": 23000, + "quantity": 1 + }, + { + "name": "Cushion Cut Diamond 3.0ct G VS1", + "name_ru": "Бриллиант огранки «Кушон» 3.0 карата G VS1", + "description": "Impressive 3.0 carat cushion cut diamond with G color and VS1 clarity. Vintage-inspired cut with modern brilliance.", + "description_ru": "Впечатляющий бриллиант огранки «Кушон» 3.0 карата с цветом G и чистотой VS1. Огранка в винтажном стиле с современным блеском.", + "category": "Diamonds", + "brand": "Sparkle & Stone", + "partnumber": "DIA-CU-300-G-VS1", + "price": 42000, + "purchase_price": 35000, + "quantity": 1 + }, + { + "name": "Emerald Cut Diamond 1.8ct D VVS2", + "name_ru": "Бриллиант изумрудной огранки 1.8 карата D VVS2", + "description": "Elegant 1.8 carat emerald cut diamond with D color and VVS2 clarity. Step-cut facets create a hall-of-mirrors effect.", + "description_ru": "Элегантный бриллиант изумрудной огранки 1.8 карата с цветом D и чистотой VVS2. Ступенчатые грани создают эффект зеркального зала.", + "category": "Diamonds", + "brand": "Lumina Treasures", + "partnumber": "DIA-EM-180-D-VVS2", + "price": 32000, + "purchase_price": 26000, + "quantity": 2 + }, + { + "name": "Fancy Yellow Diamond 2.5ct", + "name_ru": "Фантазийный жёлтый бриллиант 2.5 карата", + "description": "Magnificent 2.5 carat fancy intense yellow diamond. Radiant cut with excellent color distribution. GIA certified.", + "description_ru": "Великолепный фантазийный интенсивно-жёлтый бриллиант 2.5 карата. Огранка «Радиант» с отличным распределением цвета. Сертификат GIA.", + "category": "Diamonds", + "brand": "Lumina Treasures", + "partnumber": "DIA-FY-250", + "price": 65000, + "purchase_price": 52000, + "quantity": 1 + }, + { + "name": "Pink Diamond 0.5ct Fancy Light", + "name_ru": "Розовый бриллиант 0.5 карата Fancy Light", + "description": "Rare 0.5 carat pink diamond with fancy light pink color. Pear shape from Argyle mine. Investment piece.", + "description_ru": "Редкий розовый бриллиант 0.5 карата светло-розового цвета. Грушевидная форма из рудника Аргайл. Инвестиционный экземпляр.", + "category": "Diamonds", + "brand": "Lumina Treasures", + "partnumber": "DIA-PNK-050-FL", + "price": 125000, + "purchase_price": 100000, + "quantity": 1 + }, + { + "name": "Burmese Ruby 2.5ct Pigeon Blood", + "name_ru": "Бирманский рубин 2.5 карата «Голубиная кровь»", + "description": "Exceptional 2.5 carat Burmese ruby with coveted pigeon blood color. Unheated and untreated with GRS certificate. Extremely rare collector's gem.", + "description_ru": "Исключительный бирманский рубин 2.5 карата с желанным цветом «голубиной крови». Без нагрева и обработки, сертификат GRS. Чрезвычайно редкий коллекционный камень.", + "category": "Rubies", + "brand": "Crimson Vault", + "partnumber": "RUB-BUR-250-PB", + "price": 125000, + "purchase_price": 100000, + "quantity": 1 + }, + { + "name": "Mozambique Ruby 1.8ct Vivid Red", + "name_ru": "Мозамбикский рубин 1.8 карата насыщенно-красный", + "description": "Beautiful 1.8 carat Mozambique ruby with vivid red saturation. Minor heat treatment for enhanced clarity. Excellent value.", + "description_ru": "Прекрасный мозамбикский рубин 1.8 карата с насыщенной красной окраской. Незначительная термообработка для улучшения чистоты. Отличное соотношение цены и качества.", + "category": "Rubies", + "brand": "Crimson Vault", + "partnumber": "RUB-MOZ-180-VR", + "price": 8500, + "purchase_price": 6800, + "quantity": 4 + }, + { + "name": "Star Ruby 3.2ct Six-Ray", + "name_ru": "Звёздчатый рубин 3.2 карата с шестилучевой звездой", + "description": "Magnificent 3.2 carat star ruby displaying sharp six-ray asterism. Cabochon cut to showcase the star effect. From Sri Lanka.", + "description_ru": "Великолепный звёздчатый рубин 3.2 карата с чёткой шестилучевой звездой. Огранка кабошон для демонстрации эффекта звезды. Из Шри-Ланки.", + "category": "Rubies", + "brand": "Terra Rara", + "partnumber": "RUB-STAR-320-SR", + "price": 15000, + "purchase_price": 12000, + "quantity": 2 + }, + { + "name": "Kashmir Sapphire 3.0ct Cornflower Blue", + "name_ru": "Кашмирский сапфир 3.0 карата васильково-голубой", + "description": "Museum-quality 3.0 carat Kashmir sapphire with legendary cornflower blue color. Unheated with velvety luster. Investment grade.", + "description_ru": "Кашмирский сапфир музейного качества 3.0 карата с легендарным васильково-голубым цветом. Без нагрева, с бархатистым блеском. Инвестиционное качество.", + "category": "Sapphires", + "brand": "Azure Dreams", + "partnumber": "SAP-KAS-300-CB", + "price": 185000, + "purchase_price": 150000, + "quantity": 1 + }, + { + "name": "Ceylon Sapphire 2.2ct Royal Blue", + "name_ru": "Цейлонский сапфир 2.2 карата королевский синий", + "description": "Stunning 2.2 carat Ceylon sapphire with rich royal blue color. Excellent clarity with minor silk inclusions. Heat treated.", + "description_ru": "Потрясающий цейлонский сапфир 2.2 карата насыщенного королевского синего цвета. Отличная чистота с незначительными шёлковыми включениями. Термообработан.", + "category": "Sapphires", + "brand": "Azure Dreams", + "partnumber": "SAP-CEY-220-RB", + "price": 12500, + "purchase_price": 10000, + "quantity": 3 + }, + { + "name": "Padparadscha Sapphire 1.5ct", + "name_ru": "Сапфир падпараджа 1.5 карата", + "description": "Rare 1.5 carat padparadscha sapphire with pink-orange sunset color. Unheated Sri Lankan origin. Highly sought after by collectors.", + "description_ru": "Редкий сапфир падпараджа 1.5 карата с розово-оранжевым закатным цветом. Без нагрева, происхождение Шри-Ланка. Высоко ценится коллекционерами.", + "category": "Sapphires", + "brand": "Terra Rara", + "partnumber": "SAP-PAD-150", + "price": 45000, + "purchase_price": 36000, + "quantity": 1 + }, + { + "name": "Yellow Sapphire 4.0ct Golden", + "name_ru": "Жёлтый сапфир 4.0 карата золотистый", + "description": "Brilliant 4.0 carat yellow sapphire with intense golden color. From Sri Lanka with excellent clarity. Untreated.", + "description_ru": "Блестящий жёлтый сапфир 4.0 карата интенсивного золотистого цвета. Из Шри-Ланки с отличной чистотой. Без обработки.", + "category": "Sapphires", + "brand": "Azure Dreams", + "partnumber": "SAP-YEL-400-GD", + "price": 6500, + "purchase_price": 5200, + "quantity": 5 + }, + { + "name": "Pink Sapphire 1.8ct Hot Pink", + "name_ru": "Розовый сапфир 1.8 карата ярко-розовый", + "description": "Vibrant 1.8 carat pink sapphire with hot pink saturation. Madagascar origin with excellent transparency.", + "description_ru": "Яркий розовый сапфир 1.8 карата с насыщенным ярко-розовым цветом. Происхождение Мадагаскар с отличной прозрачностью.", + "category": "Sapphires", + "brand": "Crimson Vault", + "partnumber": "SAP-PNK-180-HP", + "price": 4200, + "purchase_price": 3400, + "quantity": 6 + }, + { + "name": "Colombian Emerald 2.8ct Muzo Green", + "name_ru": "Колумбийский изумруд 2.8 карата зелёный Музо", + "description": "Premium 2.8 carat Colombian emerald from the famous Muzo mines. Deep green color with characteristic jardín inclusions.", + "description_ru": "Премиальный колумбийский изумруд 2.8 карата из знаменитых шахт Музо. Глубокий зелёный цвет с характерными включениями «жардин».", + "category": "Emeralds", + "brand": "Evergreen Gems", + "partnumber": "EME-COL-280-MZ", + "price": 35000, + "purchase_price": 28000, + "quantity": 2 + }, + { + "name": "Zambian Emerald 3.5ct Vivid Green", + "name_ru": "Замбийский изумруд 3.5 карата насыщенно-зелёный", + "description": "Impressive 3.5 carat Zambian emerald with vivid bluish-green color. Higher clarity than Colombian stones. Minor oil treatment.", + "description_ru": "Впечатляющий замбийский изумруд 3.5 карата с насыщенным сине-зелёным цветом. Чистота выше, чем у колумбийских камней. Незначительная масляная обработка.", + "category": "Emeralds", + "brand": "Evergreen Gems", + "partnumber": "EME-ZAM-350-VG", + "price": 18500, + "purchase_price": 15000, + "quantity": 3 + }, + { + "name": "Brazilian Emerald 1.2ct Medium Green", + "name_ru": "Бразильский изумруд 1.2 карата средне-зелёный", + "description": "Beautiful 1.2 carat Brazilian emerald with medium green saturation. Good clarity with subtle inclusions. Value option.", + "description_ru": "Прекрасный бразильский изумруд 1.2 карата средней зелёной насыщенности. Хорошая чистота с незаметными включениями. Выгодный вариант.", + "category": "Emeralds", + "brand": "Evergreen Gems", + "partnumber": "EME-BRA-120-MG", + "price": 2800, + "purchase_price": 2200, + "quantity": 8 + }, + { + "name": "Australian Black Opal 5.2ct", + "name_ru": "Австралийский чёрный опал 5.2 карата", + "description": "Spectacular 5.2 carat Australian black opal from Lightning Ridge. Brilliant play-of-color with red, green, and blue flashes.", + "description_ru": "Потрясающий австралийский чёрный опал 5.2 карата из Лайтнинг Ридж. Блестящая игра цвета с красными, зелёными и синими вспышками.", + "category": "Opals", + "brand": "Terra Rara", + "partnumber": "OPL-BLK-520-LR", + "price": 28000, + "purchase_price": 22500, + "quantity": 1 + }, + { + "name": "Ethiopian Welo Opal 3.8ct", + "name_ru": "Эфиопский опал Вело 3.8 карата", + "description": "Stunning 3.8 carat Ethiopian Welo opal with hydrophane properties. Intense play-of-color with honeycomb pattern.", + "description_ru": "Потрясающий эфиопский опал Вело 3.8 карата с гидрофанными свойствами. Интенсивная игра цвета с сотовым рисунком.", + "category": "Opals", + "brand": "Terra Rara", + "partnumber": "OPL-ETH-380-WL", + "price": 3500, + "purchase_price": 2800, + "quantity": 5 + }, + { + "name": "Boulder Opal 8.5ct", + "name_ru": "Боулдер-опал 8.5 карата", + "description": "Natural 8.5 carat boulder opal with ironstone matrix. Unique patterns with veins of brilliant color.", + "description_ru": "Природный боулдер-опал 8.5 карата с железистой матрицей. Уникальные узоры с прожилками ярких цветов.", + "category": "Opals", + "brand": "Terra Rara", + "partnumber": "OPL-BLD-850", + "price": 4800, + "purchase_price": 3800, + "quantity": 3 + }, + { + "name": "Fire Opal 2.1ct Mexican Orange", + "name_ru": "Огненный опал 2.1 карата мексиканский оранжевый", + "description": "Brilliant 2.1 carat Mexican fire opal with intense orange color. Transparent with subtle play-of-color.", + "description_ru": "Блестящий мексиканский огненный опал 2.1 карата интенсивного оранжевого цвета. Прозрачный с тонкой игрой цвета.", + "category": "Opals", + "brand": "Lumina Treasures", + "partnumber": "OPL-FIRE-210-MX", + "price": 1200, + "purchase_price": 950, + "quantity": 7 + }, + { + "name": "South Sea Pearl 14mm Golden", + "name_ru": "Жемчуг Южных морей 14мм золотистый", + "description": "Luxurious 14mm South Sea pearl with deep golden color. AAA grade with excellent luster and minimal blemishes.", + "description_ru": "Роскошный жемчуг Южных морей 14мм глубокого золотистого цвета. Класс AAA с отличным блеском и минимальными дефектами.", + "category": "Pearls", + "brand": "Oceanic Pearls", + "partnumber": "PRL-SSG-14MM", + "price": 8500, + "purchase_price": 6800, + "quantity": 4 + }, + { + "name": "Tahitian Pearl 12mm Peacock", + "name_ru": "Таитянский жемчуг 12мм павлиний", + "description": "Exotic 12mm Tahitian pearl with peacock overtones. Natural dark body color with green and purple iridescence.", + "description_ru": "Экзотический таитянский жемчуг 12мм с павлиньими переливами. Природный тёмный цвет тела с зелёной и фиолетовой иризацией.", + "category": "Pearls", + "brand": "Oceanic Pearls", + "partnumber": "PRL-TAH-12MM-PC", + "price": 3200, + "purchase_price": 2500, + "quantity": 6 + }, + { + "name": "Akoya Pearl Strand 7mm", + "name_ru": "Нить жемчуга Акойя 7мм", + "description": "Classic 18-inch strand of 7mm Akoya pearls. Perfect round shape with bright white body and rose overtone.", + "description_ru": "Классическая нить жемчуга Акойя 7мм длиной 45 см. Идеально круглая форма с ярко-белым телом и розовым перламутром.", + "category": "Pearls", + "brand": "Oceanic Pearls", + "partnumber": "PRL-AKO-7MM-STR", + "price": 4500, + "purchase_price": 3600, + "quantity": 3 + }, + { + "name": "Freshwater Pearl Set", + "name_ru": "Набор пресноводного жемчуга", + "description": "Elegant set of 10 matched freshwater pearls, 9-10mm. Various pastel colors including white, pink, and lavender.", + "description_ru": "Элегантный набор из 10 подобранных пресноводных жемчужин 9-10мм. Различные пастельные цвета: белый, розовый и лавандовый.", + "category": "Pearls", + "brand": "Oceanic Pearls", + "partnumber": "PRL-FW-SET-10", + "price": 850, + "purchase_price": 680, + "quantity": 10 + }, + { + "name": "Siberian Amethyst 8.5ct Deep Purple", + "name_ru": "Сибирский аметист 8.5 карата тёмно-фиолетовый", + "description": "Premium 8.5 carat Siberian amethyst with legendary deep purple color and red flashes. Cushion cut.", + "description_ru": "Премиальный сибирский аметист 8.5 карата с легендарным тёмно-фиолетовым цветом и красными вспышками. Огранка «Кушон».", + "category": "Amethyst", + "brand": "Crystal Kingdom", + "partnumber": "AME-SIB-850-DP", + "price": 1200, + "purchase_price": 950, + "quantity": 5 + }, + { + "name": "Uruguayan Amethyst 12.3ct", + "name_ru": "Уругвайский аметист 12.3 карата", + "description": "Magnificent 12.3 carat Uruguayan amethyst with excellent saturation. Oval cut with exceptional clarity.", + "description_ru": "Великолепный уругвайский аметист 12.3 карата с отличной насыщенностью. Овальная огранка с исключительной чистотой.", + "category": "Amethyst", + "brand": "Crystal Kingdom", + "partnumber": "AME-URU-1230", + "price": 650, + "purchase_price": 520, + "quantity": 8 + }, + { + "name": "Ametrine 6.8ct Bi-Color", + "name_ru": "Аметрин 6.8 карата двухцветный", + "description": "Unique 6.8 carat ametrine showing both amethyst purple and citrine gold colors. Emerald cut from Bolivia.", + "description_ru": "Уникальный аметрин 6.8 карата, демонстрирующий фиолетовый цвет аметиста и золотистый цитрина. Изумрудная огранка, Боливия.", + "category": "Amethyst", + "brand": "Crystal Kingdom", + "partnumber": "AME-TRI-680-BC", + "price": 450, + "purchase_price": 360, + "quantity": 6 + }, + { + "name": "Santa Maria Aquamarine 4.2ct", + "name_ru": "Аквамарин Санта-Мария 4.2 карата", + "description": "Exceptional 4.2 carat Santa Maria aquamarine with intense blue color. The finest aquamarine variety from Brazil.", + "description_ru": "Исключительный аквамарин Санта-Мария 4.2 карата с интенсивным голубым цветом. Лучшая разновидность аквамарина из Бразилии.", + "category": "Aquamarine", + "brand": "Azure Dreams", + "partnumber": "AQU-SM-420", + "price": 5500, + "purchase_price": 4400, + "quantity": 2 + }, + { + "name": "Madagascar Aquamarine 7.5ct", + "name_ru": "Мадагаскарский аквамарин 7.5 карата", + "description": "Beautiful 7.5 carat Madagascar aquamarine with light blue color. Excellent clarity with octagon cut.", + "description_ru": "Прекрасный мадагаскарский аквамарин 7.5 карата светло-голубого цвета. Отличная чистота с восьмиугольной огранкой.", + "category": "Aquamarine", + "brand": "Azure Dreams", + "partnumber": "AQU-MAD-750", + "price": 2200, + "purchase_price": 1750, + "quantity": 4 + }, + { + "name": "Pakistani Aquamarine 15.2ct", + "name_ru": "Пакистанский аквамарин 15.2 карата", + "description": "Spectacular 15.2 carat aquamarine from Pakistan's Shigar Valley. Medium blue with exceptional size and clarity.", + "description_ru": "Впечатляющий аквамарин 15.2 карата из пакистанской долины Шигар. Средне-голубой с исключительным размером и чистотой.", + "category": "Aquamarine", + "brand": "Terra Rara", + "partnumber": "AQU-PAK-1520", + "price": 4800, + "purchase_price": 3850, + "quantity": 2 + }, + { + "name": "AAA Tanzanite 5.8ct Vivid Blue", + "name_ru": "Танзанит ААА 5.8 карата насыщенно-синий", + "description": "Top-quality 5.8 carat tanzanite with vivid blue-violet color. Trillion cut with exceptional saturation.", + "description_ru": "Танзанит высшего качества 5.8 карата с насыщенным сине-фиолетовым цветом. Огранка «Триллион» с исключительной насыщенностью.", + "category": "Tanzanite", + "brand": "Terra Rara", + "partnumber": "TAN-AAA-580-VB", + "price": 8500, + "purchase_price": 6800, + "quantity": 2 + }, + { + "name": "Tanzanite 3.2ct Blue-Violet", + "name_ru": "Танзанит 3.2 карата сине-фиолетовый", + "description": "Beautiful 3.2 carat tanzanite with balanced blue-violet color shift. Oval cut with good clarity.", + "description_ru": "Прекрасный танзанит 3.2 карата со сбалансированным сине-фиолетовым переходом цвета. Овальная огранка с хорошей чистотой.", + "category": "Tanzanite", + "brand": "Terra Rara", + "partnumber": "TAN-320-BV", + "price": 3200, + "purchase_price": 2560, + "quantity": 4 + }, + { + "name": "Tanzanite Pair 2.0ct Each", + "name_ru": "Пара танзанитов по 2.0 карата", + "description": "Matched pair of 2.0 carat tanzanites, perfect for earrings. Identical color and cut with excellent symmetry.", + "description_ru": "Подобранная пара танзанитов по 2.0 карата, идеальна для серёг. Идентичный цвет и огранка с отличной симметрией.", + "category": "Tanzanite", + "brand": "Terra Rara", + "partnumber": "TAN-PAIR-200", + "price": 5800, + "purchase_price": 4650, + "quantity": 3 + }, + { + "name": "Paraiba Tourmaline 1.2ct Neon Blue", + "name_ru": "Параиба турмалин 1.2 карата неоново-голубой", + "description": "Extremely rare 1.2 carat Paraiba tourmaline with electric neon blue color. Brazilian origin with copper inclusions.", + "description_ru": "Чрезвычайно редкий турмалин параиба 1.2 карата с электрическим неоново-голубым цветом. Бразильское происхождение с медными включениями.", + "category": "Tourmaline", + "brand": "Terra Rara", + "partnumber": "TOU-PAR-120-NB", + "price": 85000, + "purchase_price": 68000, + "quantity": 1 + }, + { + "name": "Watermelon Tourmaline 8.5ct", + "name_ru": "Арбузный турмалин 8.5 карата", + "description": "Stunning 8.5 carat watermelon tourmaline with pink center and green rim. Slice cut to display bi-color.", + "description_ru": "Потрясающий арбузный турмалин 8.5 карата с розовым центром и зелёным ободком. Огранка «слайс» для демонстрации двуцветности.", + "category": "Tourmaline", + "brand": "Crystal Kingdom", + "partnumber": "TOU-WM-850", + "price": 1800, + "purchase_price": 1450, + "quantity": 4 + }, + { + "name": "Rubellite Tourmaline 4.3ct", + "name_ru": "Рубеллит турмалин 4.3 карата", + "description": "Vivid 4.3 carat rubellite tourmaline with raspberry red color. Cushion cut with excellent saturation.", + "description_ru": "Яркий рубеллит турмалин 4.3 карата малиново-красного цвета. Огранка «Кушон» с отличной насыщенностью.", + "category": "Tourmaline", + "brand": "Crimson Vault", + "partnumber": "TOU-RUB-430", + "price": 3500, + "purchase_price": 2800, + "quantity": 3 + }, + { + "name": "Chrome Tourmaline 2.8ct", + "name_ru": "Хромовый турмалин 2.8 карата", + "description": "Rich 2.8 carat chrome tourmaline with intense green color. From East Africa with excellent transparency.", + "description_ru": "Насыщенный хромовый турмалин 2.8 карата интенсивного зелёного цвета. Из Восточной Африки с отличной прозрачностью.", + "category": "Tourmaline", + "brand": "Evergreen Gems", + "partnumber": "TOU-CHR-280", + "price": 2200, + "purchase_price": 1750, + "quantity": 5 + }, + { + "name": "Indicolite Tourmaline 3.6ct", + "name_ru": "Индиголит турмалин 3.6 карата", + "description": "Beautiful 3.6 carat indicolite tourmaline with teal blue color. Oval cut from Afghanistan.", + "description_ru": "Прекрасный индиголит турмалин 3.6 карата сине-зелёного цвета. Овальная огранка, Афганистан.", + "category": "Tourmaline", + "brand": "Azure Dreams", + "partnumber": "TOU-IND-360", + "price": 2800, + "purchase_price": 2250, + "quantity": 4 + } + ], + "vendor": { + "name": "Schon Demo", + "markup_percent": 5 + }, + "demo_users": { + "password": "Schon!Demo888", + "email_domain": "demo.schon.store", + "first_names": [ + "Emma", "Liam", "Olivia", "Noah", "Ava", "Ethan", "Sophia", "Mason", + "Isabella", "William", "Mia", "James", "Charlotte", "Benjamin", "Amelia", + "Lucas", "Harper", "Henry", "Evelyn", "Alexander", "Abigail", "Michael", + "Emily", "Daniel", "Elizabeth", "Jacob", "Sofia", "Logan", "Avery", + "Jackson", "Ella", "Sebastian", "Scarlett", "Aiden", "Grace", "Matthew", + "Chloe", "David", "Victoria", "Joseph", "Riley", "Carter", "Aria", + "Owen", "Lily", "Wyatt", "Aurora", "John", "Zoey", "Luke", "Nora" + ], + "last_names": [ + "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", + "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", + "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", + "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", + "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", "King", + "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", + "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", + "Carter", "Roberts" + ], + "cities": [ + {"city": "New York", "region": "NY", "postal_code": "10001", "country": "USA"}, + {"city": "Los Angeles", "region": "CA", "postal_code": "90001", "country": "USA"}, + {"city": "Chicago", "region": "IL", "postal_code": "60601", "country": "USA"}, + {"city": "Houston", "region": "TX", "postal_code": "77001", "country": "USA"}, + {"city": "Phoenix", "region": "AZ", "postal_code": "85001", "country": "USA"}, + {"city": "Philadelphia", "region": "PA", "postal_code": "19101", "country": "USA"}, + {"city": "San Antonio", "region": "TX", "postal_code": "78201", "country": "USA"}, + {"city": "San Diego", "region": "CA", "postal_code": "92101", "country": "USA"}, + {"city": "Dallas", "region": "TX", "postal_code": "75201", "country": "USA"}, + {"city": "San Jose", "region": "CA", "postal_code": "95101", "country": "USA"}, + {"city": "Austin", "region": "TX", "postal_code": "78701", "country": "USA"}, + {"city": "Jacksonville", "region": "FL", "postal_code": "32099", "country": "USA"}, + {"city": "Fort Worth", "region": "TX", "postal_code": "76101", "country": "USA"}, + {"city": "Columbus", "region": "OH", "postal_code": "43085", "country": "USA"}, + {"city": "Charlotte", "region": "NC", "postal_code": "28201", "country": "USA"}, + {"city": "London", "region": "Greater London", "postal_code": "SW1A 1AA", "country": "UK"}, + {"city": "Manchester", "region": "Greater Manchester", "postal_code": "M1 1AD", "country": "UK"}, + {"city": "Birmingham", "region": "West Midlands", "postal_code": "B1 1AA", "country": "UK"}, + {"city": "Paris", "region": "Île-de-France", "postal_code": "75001", "country": "France"}, + {"city": "Berlin", "region": "Berlin", "postal_code": "10115", "country": "Germany"}, + {"city": "Munich", "region": "Bavaria", "postal_code": "80331", "country": "Germany"}, + {"city": "Toronto", "region": "Ontario", "postal_code": "M5H 2N2", "country": "Canada"}, + {"city": "Vancouver", "region": "British Columbia", "postal_code": "V6C 1E1", "country": "Canada"}, + {"city": "Sydney", "region": "NSW", "postal_code": "2000", "country": "Australia"}, + {"city": "Melbourne", "region": "VIC", "postal_code": "3000", "country": "Australia"} + ], + "streets": [ + "Main Street", "Oak Avenue", "Maple Drive", "Park Boulevard", "Cedar Lane", + "Elm Street", "Washington Avenue", "Lake Street", "Hill Road", "Forest Drive", + "River Road", "Sunset Boulevard", "Highland Avenue", "Valley View Drive", + "Mountain Road" + ] + } +} diff --git a/engine/core/fixtures/demo_products_images/AME-SIB-850-DP.jpg b/engine/core/fixtures/demo_products_images/AME-SIB-850-DP.jpg new file mode 100644 index 00000000..9e30bfee Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AME-SIB-850-DP.jpg differ diff --git a/engine/core/fixtures/demo_products_images/AME-TRI-680-BC.jpg b/engine/core/fixtures/demo_products_images/AME-TRI-680-BC.jpg new file mode 100644 index 00000000..26ecc0d4 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AME-TRI-680-BC.jpg differ diff --git a/engine/core/fixtures/demo_products_images/AME-URU-1230.jpg b/engine/core/fixtures/demo_products_images/AME-URU-1230.jpg new file mode 100644 index 00000000..cd212d34 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AME-URU-1230.jpg differ diff --git a/engine/core/fixtures/demo_products_images/AQU-SM-420.jpg b/engine/core/fixtures/demo_products_images/AQU-SM-420.jpg new file mode 100644 index 00000000..977e6842 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AQU-SM-420.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-CU-300-G-VS1.jpg b/engine/core/fixtures/demo_products_images/DIA-CU-300-G-VS1.jpg new file mode 100644 index 00000000..15866368 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-CU-300-G-VS1.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-EM-180-D-VVS2.jpg b/engine/core/fixtures/demo_products_images/DIA-EM-180-D-VVS2.jpg new file mode 100644 index 00000000..11e6ce7f Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-EM-180-D-VVS2.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-FY-250.jpg b/engine/core/fixtures/demo_products_images/DIA-FY-250.jpg new file mode 100644 index 00000000..13e7dab0 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-FY-250.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-OV-120-F-IF.jpg b/engine/core/fixtures/demo_products_images/DIA-OV-120-F-IF.jpg new file mode 100644 index 00000000..94ed9ca4 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-OV-120-F-IF.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-PC-200-E-VS2.jpg b/engine/core/fixtures/demo_products_images/DIA-PC-200-E-VS2.jpg new file mode 100644 index 00000000..50e96ed7 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-PC-200-E-VS2.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-PNK-050-FL.jpg b/engine/core/fixtures/demo_products_images/DIA-PNK-050-FL.jpg new file mode 100644 index 00000000..2f3acc4f Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-PNK-050-FL.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-RB-150-D-VVS1.jpg b/engine/core/fixtures/demo_products_images/DIA-RB-150-D-VVS1.jpg new file mode 100644 index 00000000..17ce695b Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-RB-150-D-VVS1.jpg differ diff --git a/engine/core/fixtures/demo_products_images/EME-BRA-120-MG.jpg b/engine/core/fixtures/demo_products_images/EME-BRA-120-MG.jpg new file mode 100644 index 00000000..19d5cd64 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/EME-BRA-120-MG.jpg differ diff --git a/engine/core/fixtures/demo_products_images/EME-COL-280-MZ.jpg b/engine/core/fixtures/demo_products_images/EME-COL-280-MZ.jpg new file mode 100644 index 00000000..b283bb2d Binary files /dev/null and b/engine/core/fixtures/demo_products_images/EME-COL-280-MZ.jpg differ diff --git a/engine/core/fixtures/demo_products_images/EME-ZAM-350-VG.jpg b/engine/core/fixtures/demo_products_images/EME-ZAM-350-VG.jpg new file mode 100644 index 00000000..fdec0ae0 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/EME-ZAM-350-VG.jpg differ diff --git a/engine/core/fixtures/demo_products_images/OPL-BLD-850.jpg b/engine/core/fixtures/demo_products_images/OPL-BLD-850.jpg new file mode 100644 index 00000000..b250246d Binary files /dev/null and b/engine/core/fixtures/demo_products_images/OPL-BLD-850.jpg differ diff --git a/engine/core/fixtures/demo_products_images/OPL-BLK-520-LR.jpg b/engine/core/fixtures/demo_products_images/OPL-BLK-520-LR.jpg new file mode 100644 index 00000000..1d31350f Binary files /dev/null and b/engine/core/fixtures/demo_products_images/OPL-BLK-520-LR.jpg differ diff --git a/engine/core/fixtures/demo_products_images/OPL-ETH-380-WL.jpg b/engine/core/fixtures/demo_products_images/OPL-ETH-380-WL.jpg new file mode 100644 index 00000000..66812b63 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/OPL-ETH-380-WL.jpg differ diff --git a/engine/core/fixtures/demo_products_images/OPL-FIRE-210-MX.jpg b/engine/core/fixtures/demo_products_images/OPL-FIRE-210-MX.jpg new file mode 100644 index 00000000..586c3d40 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/OPL-FIRE-210-MX.jpg differ diff --git a/engine/core/fixtures/demo_products_images/PRL-AKO-7MM-STR.jpg b/engine/core/fixtures/demo_products_images/PRL-AKO-7MM-STR.jpg new file mode 100644 index 00000000..5e5bf6cf Binary files /dev/null and b/engine/core/fixtures/demo_products_images/PRL-AKO-7MM-STR.jpg differ diff --git a/engine/core/fixtures/demo_products_images/PRL-FW-SET-10.jpg b/engine/core/fixtures/demo_products_images/PRL-FW-SET-10.jpg new file mode 100644 index 00000000..7e7ed221 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/PRL-FW-SET-10.jpg differ diff --git a/engine/core/fixtures/demo_products_images/PRL-SSG-14MM.jpg b/engine/core/fixtures/demo_products_images/PRL-SSG-14MM.jpg new file mode 100644 index 00000000..a00d44a6 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/PRL-SSG-14MM.jpg differ diff --git a/engine/core/fixtures/demo_products_images/RUB-BUR-250-PB.jpg b/engine/core/fixtures/demo_products_images/RUB-BUR-250-PB.jpg new file mode 100644 index 00000000..0a4df056 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/RUB-BUR-250-PB.jpg differ diff --git a/engine/core/fixtures/demo_products_images/RUB-MOZ-180-VR.jpg b/engine/core/fixtures/demo_products_images/RUB-MOZ-180-VR.jpg new file mode 100644 index 00000000..ada20bd0 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/RUB-MOZ-180-VR.jpg differ diff --git a/engine/core/fixtures/demo_products_images/RUB-STAR-320-SR.jpg b/engine/core/fixtures/demo_products_images/RUB-STAR-320-SR.jpg new file mode 100644 index 00000000..77236d39 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/RUB-STAR-320-SR.jpg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-CEY-220-RB.jpg b/engine/core/fixtures/demo_products_images/SAP-CEY-220-RB.jpg new file mode 100644 index 00000000..7c7b15cc Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-CEY-220-RB.jpg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-KAS-300-CB.jpg b/engine/core/fixtures/demo_products_images/SAP-KAS-300-CB.jpg new file mode 100644 index 00000000..1718529f Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-KAS-300-CB.jpg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-PNK-180-HP.jpg b/engine/core/fixtures/demo_products_images/SAP-PNK-180-HP.jpg new file mode 100644 index 00000000..49fc28d9 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-PNK-180-HP.jpg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-YEL-400-GD.jpg b/engine/core/fixtures/demo_products_images/SAP-YEL-400-GD.jpg new file mode 100644 index 00000000..63608cdc Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-YEL-400-GD.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TAN-AAA-580-VB.jpg b/engine/core/fixtures/demo_products_images/TAN-AAA-580-VB.jpg new file mode 100644 index 00000000..99f6541d Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TAN-AAA-580-VB.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TAN-PAIR-200.jpg b/engine/core/fixtures/demo_products_images/TAN-PAIR-200.jpg new file mode 100644 index 00000000..0c539b60 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TAN-PAIR-200.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TOU-IND-360.jpg b/engine/core/fixtures/demo_products_images/TOU-IND-360.jpg new file mode 100644 index 00000000..a201a5bd Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TOU-IND-360.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TOU-PAR-120-NB.jpg b/engine/core/fixtures/demo_products_images/TOU-PAR-120-NB.jpg new file mode 100644 index 00000000..31bf53ad Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TOU-PAR-120-NB.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TOU-WM-850.jpg b/engine/core/fixtures/demo_products_images/TOU-WM-850.jpg new file mode 100644 index 00000000..c38fad71 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TOU-WM-850.jpg differ diff --git a/engine/core/fixtures/demo_products_images/placeholder.png b/engine/core/fixtures/demo_products_images/placeholder.png new file mode 100644 index 00000000..448cd452 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/placeholder.png differ diff --git a/engine/core/models.py b/engine/core/models.py index 20f905b0..0ad745aa 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -39,6 +39,7 @@ from django.db.models import ( from django.db.models.functions import Length from django.db.models.indexes import Index from django.http import Http404 +from django.templatetags.static import static from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.functional import cached_property @@ -436,9 +437,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): @cached_property def image_url(self) -> str: with suppress(ValueError): - url = str(self.image.url) - return url if "http" in url else f"https://api.{settings.BASE_DOMAIN}{url}" - return "" + if self.image: + return self.image.url + # Fallback to favicon.png from static files + return static("favicon.png") class Meta: verbose_name = _("category") @@ -893,9 +895,10 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel): @cached_property def image_url(self) -> str: with suppress(ValueError): - url = str(self.image.url) - return url if "http" in url else f"https://api.{settings.BASE_DOMAIN}{url}" - return "" + if self.image: + return self.image.url + # Fallback to favicon.png from static files + return static("favicon.png") class Meta: ordering = ("priority",) diff --git a/engine/core/static/robots_frontend.txt b/engine/core/static/robots_frontend.txt index ff35bfa3..b823f243 100644 --- a/engine/core/static/robots_frontend.txt +++ b/engine/core/static/robots_frontend.txt @@ -7,5 +7,5 @@ Disallow: /*/auth/sign-up/ Allow: / -Sitemap: https://schon.com/sitemap.xml -Host: schon.com \ No newline at end of file +Sitemap: https://schon.fureunoir.com/sitemap.xml +Host: schon.fureunoir.com \ No newline at end of file diff --git a/engine/core/utils/commerce.py b/engine/core/utils/commerce.py index 4a9e2c91..c52254e1 100644 --- a/engine/core/utils/commerce.py +++ b/engine/core/utils/commerce.py @@ -65,11 +65,21 @@ def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)) -> f def get_returns(period: timedelta = timedelta(days=30)) -> float: - order_products = get_period_order_products(period, ["RETURNED"]) + """Get total value of returned order products within the period. + + Returns are counted regardless of order status - a RETURNED OrderProduct + counts as a return whether the order is FINISHED or FAILED. + """ + current = now() + period_start = current - period total_returns: float = ( - order_products.aggregate( - total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0) - ).get("total") + OrderProduct.objects.filter( + status="RETURNED", + order__buy_time__lte=current, + order__buy_time__gte=period_start, + ) + .aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)) + .get("total") or 0.0 ) try: @@ -131,12 +141,16 @@ def get_daily_gross_revenue( def get_top_returned_products( period: timedelta = timedelta(days=30), limit: int = 10 ) -> list[dict[str, Any]]: + """Get top returned products within the period. + + Returns are counted regardless of order status - a RETURNED OrderProduct + counts as a return whether the order is FINISHED or FAILED. + """ current = now() period_start = current - period qs = ( OrderProduct.objects.filter( status="RETURNED", - order__status="FINISHED", order__buy_time__lte=current, order__buy_time__gte=period_start, product__isnull=False, diff --git a/engine/core/views.py b/engine/core/views.py index 9f32807a..d7e1a808 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -2,7 +2,6 @@ import logging import mimetypes import os import traceback -from contextlib import suppress from datetime import date, timedelta import requests @@ -461,56 +460,44 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: returns_cur: float = get_returns(period=period) processed_orders_cur: int = get_total_processed_orders(period=period) - orders_finished_cur = 0 - - with suppress(Exception): - orders_finished_cur: int = Order.objects.filter( - status="FINISHED", buy_time__lte=now_dt, buy_time__gte=cur_start - ).count() + orders_finished_cur: int = Order.objects.filter( + status="FINISHED", buy_time__lte=now_dt, buy_time__gte=cur_start + ).count() def sum_gross_between(start: date | None, end: date | None) -> float: - result = 0.0 - with suppress(Exception): - qs = ( - OrderProduct.objects.filter( - status__in=["FINISHED"], order__status="FINISHED" - ) - .filter(order__buy_time__lt=end, order__buy_time__gte=start) - .aggregate(total=Sum(F("buy_price") * F("quantity"))) + qs = ( + OrderProduct.objects.filter( + status__in=["FINISHED"], order__status="FINISHED" ) - total = qs.get("total") or 0.0 - result = round(float(total), 2) + .filter(order__buy_time__lt=end, order__buy_time__gte=start) + .aggregate(total=Sum(F("buy_price") * F("quantity"))) + ) + total = qs.get("total") or 0.0 + result = round(float(total), 2) return result def sum_returns_between(start: date | None, end: date | None) -> float: - result = 0.0 - with suppress(Exception): - qs = ( - OrderProduct.objects.filter(status__in=["RETURNED"]) # returned items - .filter(order__buy_time__lt=end, order__buy_time__gte=start) - .aggregate(total=Sum(F("buy_price") * F("quantity"))) - ) - total = qs.get("total") or 0.0 - result = round(float(total), 2) + qs = ( + OrderProduct.objects.filter(status__in=["RETURNED"]) # returned items + .filter(order__buy_time__lt=end, order__buy_time__gte=start) + .aggregate(total=Sum(F("buy_price") * F("quantity"))) + ) + total = qs.get("total") or 0.0 + result = round(float(total), 2) return result def count_finished_orders_between(start: date | None, end: date | None) -> int: - result = 0 - with suppress(Exception): - result = Order.objects.filter( - status="FINISHED", buy_time__lt=end, buy_time__gte=start - ).count() + result = Order.objects.filter( + status="FINISHED", buy_time__lt=end, buy_time__gte=start + ).count() return result revenue_gross_prev = sum_gross_between(prev_start, prev_end) returns_prev = sum_returns_between(prev_start, prev_end) orders_finished_prev = count_finished_orders_between(prev_start, prev_end) - tax_rate = 0.0 - tax_included = False - with suppress(Exception): - tax_rate = float(getattr(config, "TAX_RATE", 0.0) or 0.0) - tax_included = bool(getattr(config, "TAX_INCLUDED", False)) + tax_rate = float(getattr(config, "TAX_RATE", 0.0) or 0.0) + tax_included = bool(getattr(config, "TAX_INCLUDED", False)) if tax_rate <= 0: revenue_net_prev = revenue_gross_prev @@ -525,12 +512,11 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: revenue_net_prev = round(float(revenue_net_prev or 0.0), 2) def pct_delta(cur: float | int, prev: float | int) -> float: - result = 0.0 - with suppress(Exception): - cur_f = float(cur or 0) - prev_f = float(prev or 0) - if prev_f == 0: - result = 0.0 if cur_f == 0 else 100.0 + cur_f = float(cur or 0) + prev_f = float(prev or 0) + if prev_f == 0: + result = 0.0 if cur_f == 0 else 100.0 + else: result = round(((cur_f - prev_f) / prev_f) * 100.0, 1) return result @@ -584,13 +570,15 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: }, } currency_symbol: str = "" - with suppress(Exception): + try: currency_symbol = dict(getattr(settings, "CURRENCIES_WITH_SYMBOLS", ())).get( getattr(settings, "CURRENCY_CODE", ""), "" ) + except Exception as exc: + logger.error("Failed to get currency symbol: %s", exc) quick_links: list[dict[str, str]] = [] - with suppress(Exception): + try: quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[ 1 ] @@ -606,10 +594,12 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: **({"icon": item.get("icon")} if item.get("icon") else {}), } ) + except Exception as exc: + logger.error("Failed to build quick links: %s", exc) most_wished: dict[str, str | int | float | None] | None = None most_wished_list: list[dict[str, str | int | float | None]] = [] - with suppress(Exception): + try: wished_qs = ( Wishlist.objects.filter(user__is_active=True, user__is_staff=False) .values("products") @@ -655,6 +645,8 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "count": int(row.get("cnt", 0)), } ) + except Exception as exc: + logger.error("Failed to build most wished list: %s", exc) try: today = tz_now().date() @@ -679,7 +671,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: context["daily_labels"] = [] context["daily_orders"] = [] context["daily_gross"] = [] - with suppress(Exception): + try: today = tz_now().date() days = period_days date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)] @@ -689,12 +681,14 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: context["daily_title"] = _("Revenue & Orders (last %(days)d)") % { "days": period_days } + except Exception as exc: + logger.error("Failed to build daily stats: %s", exc) low_stock_list: list[dict[str, str | int]] = [] - with suppress(Exception): + try: products = ( Product.objects.annotate(total_qty=Sum("stocks__quantity")) - .values("id", "name", "sku", "total_qty") + .values("uuid", "name", "sku", "total_qty") .order_by("total_qty")[:5] ) for p in products: @@ -709,11 +703,11 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: ), } ) + except Exception as exc: + logger.error(f"Error fetching low stock products: {exc}") cache_key = f"dashboard_cb:{period_days}" - cached_pack = None - with suppress(Exception): - cached_pack = cache.get(cache_key) + cached_pack = cache.get(cache_key, None) if cached_pack is None: cached_pack = { "kpi": kpi, @@ -725,12 +719,11 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "refund_rate": refund_rate_cur, "low_stock_products": low_stock_list, } - with suppress(Exception): - cache.set(cache_key, cached_pack, 600) + cache.set(cache_key, cached_pack, 600) most_popular: dict[str, str | int | float | None] | None = None most_popular_list: list[dict[str, str | int | float | None]] = [] - with suppress(Exception): + try: popular_qs = ( OrderProduct.objects.filter( status="FINISHED", order__status="FINISHED", product__isnull=False @@ -777,6 +770,8 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "count": int(row.get("total_qty", 0) or 0), } ) + except Exception as exc: + logger.error("Failed to build most popular list: %s", exc) customers_mix: dict[str, int | float] = { "new": 0, @@ -784,7 +779,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "new_pct": 0.0, "returning_pct": 0.0, } - with suppress(Exception): + try: mix = get_customer_mix() n = int(mix.get("new", 0)) r = int(mix.get("returning", 0)) @@ -798,6 +793,8 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "returning_pct": ret_pct, "total": t, } + except Exception as exc: + logger.error("Failed to build customer mix: %s", exc) shipped_vs_digital: dict[str, int | float] = { "digital_qty": 0, @@ -807,7 +804,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "digital_pct": 0.0, "shipped_pct": 0.0, } - with suppress(Exception): + try: svd = get_shipped_vs_digital_mix() dq = int(svd.get("digital_qty", 0)) sq = int(svd.get("shipped_qty", 0)) @@ -824,14 +821,12 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: "shipped_pct": shipped_pct, } ) + except Exception as exc: + logger.error("Failed to build shipped vs digital mix: %s", exc) - most_returned_products: list[dict[str, str | int | float]] = [] - with suppress(Exception): - most_returned_products = get_top_returned_products() + most_returned_products = get_top_returned_products() - top_categories: list[dict[str, str | int | float]] = [] - with suppress(Exception): - top_categories = get_top_categories_by_qty() + top_categories = get_top_categories_by_qty() context.update( { diff --git a/nginx.example.conf b/nginx.example.conf index b6bd9d11..68b9b04e 100644 --- a/nginx.example.conf +++ b/nginx.example.conf @@ -12,10 +12,10 @@ upstream storefront_frontend { server { listen 443 ssl http2; - server_name api.schon.com; + server_name api.schon.fureunoir.com; - ssl_certificate /etc/letsencrypt/live/schon.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/schon.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_session_cache shared:SSL:10m; @@ -72,10 +72,10 @@ server { server { listen 443 ssl http2; - server_name schon.com www.schon.com; + server_name schon.fureunoir.com www.schon.fureunoir.com; - ssl_certificate /etc/letsencrypt/live/schon.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/schon.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_session_cache shared:SSL:10m; @@ -128,10 +128,10 @@ server { server { listen 443 ssl http2; - server_name prometheus.schon.com; + server_name prometheus.schon.fureunoir.com; - ssl_certificate /etc/letsencrypt/live/schon.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/schon.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_session_cache shared:SSL:10m; @@ -173,6 +173,6 @@ server { server { listen 80; - server_name api.schon.com www.schon.com schon.com prometheus.schon.com; + server_name api.schon.fureunoir.com www.schon.fureunoir.com schon.fureunoir.com prometheus.schon.fureunoir.com; return 301 https://$host$request_uri; } diff --git a/schon/settings/base.py b/schon/settings/base.py index b5cddf45..9bd561bf 100644 --- a/schon/settings/base.py +++ b/schon/settings/base.py @@ -361,10 +361,10 @@ TIME_ZONE: str = getenv("TIME_ZONE", "Europe/London") WHITENOISE_MANIFEST_STRICT: bool = False -STATIC_URL: str = f"https://api.{BASE_DOMAIN}/static/" if INITIALIZED else "static/" +STATIC_URL: str = f"https://api.{BASE_DOMAIN}/static/" if INITIALIZED else "/static/" STATIC_ROOT: Path = BASE_DIR / "static" -MEDIA_URL: str = f"https://api.{BASE_DOMAIN}/media/" if INITIALIZED else "media/" +MEDIA_URL: str = f"https://api.{BASE_DOMAIN}/media/" if INITIALIZED else "/media/" MEDIA_ROOT: Path = BASE_DIR / "media" AUTH_USER_MODEL: str = "vibes_auth.User" diff --git a/schon/urls.py b/schon/urls.py index 47f2e94a..c6ef2a26 100644 --- a/schon/urls.py +++ b/schon/urls.py @@ -1,3 +1,5 @@ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from django.views.decorators.csrf import csrf_exempt @@ -97,3 +99,6 @@ urlpatterns = [ admin.site.urls, ), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/scripts/Unix/generate-environment-file.sh b/scripts/Unix/generate-environment-file.sh index 2ad4c04a..f907e9d7 100755 --- a/scripts/Unix/generate-environment-file.sh +++ b/scripts/Unix/generate-environment-file.sh @@ -44,8 +44,8 @@ if [ -f .env ]; then fi SCHON_PROJECT_NAME=$(prompt_default SCHON_PROJECT_NAME Schon) -SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.com) -SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.com) +SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.fureunoir.com) +SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.fureunoir.com) SENTRY_DSN=$(prompt_default SENTRY_DSN "") DEBUG=$(prompt_default DEBUG 1) TIME_ZONE=$(prompt_default TIME_ZONE "Europe/London") @@ -53,8 +53,8 @@ TIME_ZONE=$(prompt_default TIME_ZONE "Europe/London") SECRET_KEY=$(prompt_autogen SECRET_KEY 32) JWT_SIGNING_KEY=$(prompt_autogen JWT_SIGNING_KEY 64) -ALLOWED_HOSTS=$(prompt_default ALLOWED_HOSTS "schon.com api.schon.com") -CSRF_TRUSTED_ORIGINS=$(prompt_default CSRF_TRUSTED_ORIGINS "https://schon.com https://api.schon.com https://www.schon.com") +ALLOWED_HOSTS=$(prompt_default ALLOWED_HOSTS "schon.fureunoir.com api.schon.fureunoir.com") +CSRF_TRUSTED_ORIGINS=$(prompt_default CSRF_TRUSTED_ORIGINS "https://schon.fureunoir.com https://api.schon.fureunoir.com https://www.schon.fureunoir.com") CORS_ALLOWED_ORIGINS=$(prompt_default CORS_ALLOWED_ORIGINS "$CSRF_TRUSTED_ORIGINS") POSTGRES_DB=$(prompt_default POSTGRES_DB schon) @@ -73,11 +73,11 @@ PROMETHEUS_USER=$(prompt_default PROMETHEUS_USER schon) PROMETHEUS_PASSWORD=$(prompt_autogen PROMETHEUS_PASSWORD 16) EMAIL_BACKEND=$(prompt_default EMAIL_BACKEND django.core.mail.backends.smtp.EmailBackend) -EMAIL_HOST=$(prompt_default EMAIL_HOST smtp.whatever.schon.com) +EMAIL_HOST=$(prompt_default EMAIL_HOST smtp.whatever.schon.fureunoir.com) EMAIL_PORT=$(prompt_default EMAIL_PORT 465) EMAIL_USE_TLS=$(prompt_default EMAIL_USE_TLS 0) EMAIL_USE_SSL=$(prompt_default EMAIL_USE_SSL 1) -EMAIL_HOST_USER=$(prompt_default EMAIL_HOST_USER your-email-user@whatever.schon.com) +EMAIL_HOST_USER=$(prompt_default EMAIL_HOST_USER your-email-user@whatever.schon.fureunoir.com) EMAIL_FROM=$EMAIL_HOST_USER EMAIL_HOST_PASSWORD=$(prompt_default EMAIL_HOST_PASSWORD SUPERSECRETEMAILHOSTPASSWORD) diff --git a/scripts/Windows/generate-environment-file.ps1 b/scripts/Windows/generate-environment-file.ps1 index bc26ea73..ed128720 100644 --- a/scripts/Windows/generate-environment-file.ps1 +++ b/scripts/Windows/generate-environment-file.ps1 @@ -50,8 +50,8 @@ if (Test-Path '.env') } $SCHON_PROJECT_NAME = Prompt-Default 'SCHON_PROJECT_NAME' 'Schon' -$SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.com' -$SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.com' +$SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.fureunoir.com' +$SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.fureunoir.com' $SENTRY_DSN = Prompt-Default 'SENTRY_DSN' '' $DEBUG = Prompt-Default 'DEBUG' '1' $TIME_ZONE = Prompt-Default 'TIME_ZONE' 'Europe/London' @@ -59,8 +59,8 @@ $TIME_ZONE = Prompt-Default 'TIME_ZONE' 'Europe/London' $SECRET_KEY = Prompt-AutoGen 'SECRET_KEY' 32 $JWT_SIGNING_KEY = Prompt-AutoGen 'JWT_SIGNING_KEY' 64 -$ALLOWED_HOSTS = Prompt-Default 'ALLOWED_HOSTS' 'schon.com api.schon.com' -$CSRF_TRUSTED_ORIGINS = Prompt-Default 'CSRF_TRUSTED_ORIGINS' 'https://schon.com https://api.schon.com https://www.schon.com' +$ALLOWED_HOSTS = Prompt-Default 'ALLOWED_HOSTS' 'schon.fureunoir.com api.schon.fureunoir.com' +$CSRF_TRUSTED_ORIGINS = Prompt-Default 'CSRF_TRUSTED_ORIGINS' 'https://schon.fureunoir.com https://api.schon.fureunoir.com https://www.schon.fureunoir.com' $CORS_ALLOWED_ORIGINS = Prompt-Default 'CORS_ALLOWED_ORIGINS' $CSRF_TRUSTED_ORIGINS $POSTGRES_DB = Prompt-Default 'POSTGRES_DB' 'schon' @@ -80,11 +80,11 @@ $PROMETHEUS_USER = Prompt-Default 'PROMETHEUS_USER' 'schon' $PROMETHEUS_PASSWORD = Prompt-AutoGen 'PROMETHEUS_PASSWORD' 16 $EMAIL_BACKEND = Prompt-Default 'EMAIL_BACKEND' 'django.core.mail.backends.smtp.EmailBackend' -$EMAIL_HOST = Prompt-Default 'EMAIL_HOST' 'smtp.whatever.schon.com' +$EMAIL_HOST = Prompt-Default 'EMAIL_HOST' 'smtp.whatever.schon.fureunoir.com' $EMAIL_PORT = Prompt-Default 'EMAIL_PORT' '465' $EMAIL_USE_TLS = Prompt-Default 'EMAIL_USE_TLS' '0' $EMAIL_USE_SSL = Prompt-Default 'EMAIL_USE_SSL' '1' -$EMAIL_HOST_USER = Prompt-Default 'EMAIL_HOST_USER' 'your-email-user@whatever.schon.com' +$EMAIL_HOST_USER = Prompt-Default 'EMAIL_HOST_USER' 'your-email-user@whatever.schon.fureunoir.com' $EMAIL_FROM = Prompt-Default 'EMAIL_FROM' $EMAIL_HOST_USER $EMAIL_HOST_PASSWORD = Prompt-Default 'EMAIL_HOST_PASSWORD' 'SUPERSECRETEMAILHOSTPASSWORD'