feat(fixtures): add demo.json for gemstone and jewelry showcase

Includes categories, brands, attributes, products, tags, and vendor/demo user data for demonstration purposes.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-02-05 15:40:57 +03:00
parent 82f4381fcb
commit f3260686cf
44 changed files with 826 additions and 99 deletions

View file

@ -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"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

View file

@ -39,6 +39,7 @@ from django.db.models import (
from django.db.models.functions import Length from django.db.models.functions import Length
from django.db.models.indexes import Index from django.db.models.indexes import Index
from django.http import Http404 from django.http import Http404
from django.templatetags.static import static
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -436,9 +437,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
@cached_property @cached_property
def image_url(self) -> str: def image_url(self) -> str:
with suppress(ValueError): with suppress(ValueError):
url = str(self.image.url) if self.image:
return url if "http" in url else f"https://api.{settings.BASE_DOMAIN}{url}" return self.image.url
return "" # Fallback to favicon.png from static files
return static("favicon.png")
class Meta: class Meta:
verbose_name = _("category") verbose_name = _("category")
@ -893,9 +895,10 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel):
@cached_property @cached_property
def image_url(self) -> str: def image_url(self) -> str:
with suppress(ValueError): with suppress(ValueError):
url = str(self.image.url) if self.image:
return url if "http" in url else f"https://api.{settings.BASE_DOMAIN}{url}" return self.image.url
return "" # Fallback to favicon.png from static files
return static("favicon.png")
class Meta: class Meta:
ordering = ("priority",) ordering = ("priority",)

View file

@ -7,5 +7,5 @@ Disallow: /*/auth/sign-up/
Allow: / Allow: /
Sitemap: https://schon.com/sitemap.xml Sitemap: https://schon.fureunoir.com/sitemap.xml
Host: schon.com Host: schon.fureunoir.com

View file

@ -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: 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 = ( total_returns: float = (
order_products.aggregate( OrderProduct.objects.filter(
total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0) status="RETURNED",
).get("total") 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 or 0.0
) )
try: try:
@ -131,12 +141,16 @@ def get_daily_gross_revenue(
def get_top_returned_products( def get_top_returned_products(
period: timedelta = timedelta(days=30), limit: int = 10 period: timedelta = timedelta(days=30), limit: int = 10
) -> list[dict[str, Any]]: ) -> 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() current = now()
period_start = current - period period_start = current - period
qs = ( qs = (
OrderProduct.objects.filter( OrderProduct.objects.filter(
status="RETURNED", status="RETURNED",
order__status="FINISHED",
order__buy_time__lte=current, order__buy_time__lte=current,
order__buy_time__gte=period_start, order__buy_time__gte=period_start,
product__isnull=False, product__isnull=False,

View file

@ -2,7 +2,6 @@ import logging
import mimetypes import mimetypes
import os import os
import traceback import traceback
from contextlib import suppress
from datetime import date, timedelta from datetime import date, timedelta
import requests import requests
@ -461,56 +460,44 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
returns_cur: float = get_returns(period=period) returns_cur: float = get_returns(period=period)
processed_orders_cur: int = get_total_processed_orders(period=period) processed_orders_cur: int = get_total_processed_orders(period=period)
orders_finished_cur = 0 orders_finished_cur: int = Order.objects.filter(
status="FINISHED", buy_time__lte=now_dt, buy_time__gte=cur_start
with suppress(Exception): ).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: def sum_gross_between(start: date | None, end: date | None) -> float:
result = 0.0 qs = (
with suppress(Exception): OrderProduct.objects.filter(
qs = ( status__in=["FINISHED"], order__status="FINISHED"
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")))
) )
total = qs.get("total") or 0.0 .filter(order__buy_time__lt=end, order__buy_time__gte=start)
result = round(float(total), 2) .aggregate(total=Sum(F("buy_price") * F("quantity")))
)
total = qs.get("total") or 0.0
result = round(float(total), 2)
return result return result
def sum_returns_between(start: date | None, end: date | None) -> float: def sum_returns_between(start: date | None, end: date | None) -> float:
result = 0.0 qs = (
with suppress(Exception): OrderProduct.objects.filter(status__in=["RETURNED"]) # returned items
qs = ( .filter(order__buy_time__lt=end, order__buy_time__gte=start)
OrderProduct.objects.filter(status__in=["RETURNED"]) # returned items .aggregate(total=Sum(F("buy_price") * F("quantity")))
.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)
total = qs.get("total") or 0.0
result = round(float(total), 2)
return result return result
def count_finished_orders_between(start: date | None, end: date | None) -> int: def count_finished_orders_between(start: date | None, end: date | None) -> int:
result = 0 result = Order.objects.filter(
with suppress(Exception): status="FINISHED", buy_time__lt=end, buy_time__gte=start
result = Order.objects.filter( ).count()
status="FINISHED", buy_time__lt=end, buy_time__gte=start
).count()
return result return result
revenue_gross_prev = sum_gross_between(prev_start, prev_end) revenue_gross_prev = sum_gross_between(prev_start, prev_end)
returns_prev = sum_returns_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) orders_finished_prev = count_finished_orders_between(prev_start, prev_end)
tax_rate = 0.0 tax_rate = float(getattr(config, "TAX_RATE", 0.0) or 0.0)
tax_included = False tax_included = bool(getattr(config, "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))
if tax_rate <= 0: if tax_rate <= 0:
revenue_net_prev = revenue_gross_prev 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) revenue_net_prev = round(float(revenue_net_prev or 0.0), 2)
def pct_delta(cur: float | int, prev: float | int) -> float: def pct_delta(cur: float | int, prev: float | int) -> float:
result = 0.0 cur_f = float(cur or 0)
with suppress(Exception): prev_f = float(prev or 0)
cur_f = float(cur or 0) if prev_f == 0:
prev_f = float(prev or 0) result = 0.0 if cur_f == 0 else 100.0
if prev_f == 0: else:
result = 0.0 if cur_f == 0 else 100.0
result = round(((cur_f - prev_f) / prev_f) * 100.0, 1) result = round(((cur_f - prev_f) / prev_f) * 100.0, 1)
return result return result
@ -584,13 +570,15 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
}, },
} }
currency_symbol: str = "" currency_symbol: str = ""
with suppress(Exception): try:
currency_symbol = dict(getattr(settings, "CURRENCIES_WITH_SYMBOLS", ())).get( currency_symbol = dict(getattr(settings, "CURRENCIES_WITH_SYMBOLS", ())).get(
getattr(settings, "CURRENCY_CODE", ""), "" getattr(settings, "CURRENCY_CODE", ""), ""
) )
except Exception as exc:
logger.error("Failed to get currency symbol: %s", exc)
quick_links: list[dict[str, str]] = [] quick_links: list[dict[str, str]] = []
with suppress(Exception): try:
quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[ quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[
1 1
] ]
@ -606,10 +594,12 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
**({"icon": item.get("icon")} if item.get("icon") else {}), **({"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: dict[str, str | int | float | None] | None = None
most_wished_list: list[dict[str, str | int | float | None]] = [] most_wished_list: list[dict[str, str | int | float | None]] = []
with suppress(Exception): try:
wished_qs = ( wished_qs = (
Wishlist.objects.filter(user__is_active=True, user__is_staff=False) Wishlist.objects.filter(user__is_active=True, user__is_staff=False)
.values("products") .values("products")
@ -655,6 +645,8 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"count": int(row.get("cnt", 0)), "count": int(row.get("cnt", 0)),
} }
) )
except Exception as exc:
logger.error("Failed to build most wished list: %s", exc)
try: try:
today = tz_now().date() today = tz_now().date()
@ -679,7 +671,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
context["daily_labels"] = [] context["daily_labels"] = []
context["daily_orders"] = [] context["daily_orders"] = []
context["daily_gross"] = [] context["daily_gross"] = []
with suppress(Exception): try:
today = tz_now().date() today = tz_now().date()
days = period_days days = period_days
date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)] 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)") % { context["daily_title"] = _("Revenue & Orders (last %(days)d)") % {
"days": period_days "days": period_days
} }
except Exception as exc:
logger.error("Failed to build daily stats: %s", exc)
low_stock_list: list[dict[str, str | int]] = [] low_stock_list: list[dict[str, str | int]] = []
with suppress(Exception): try:
products = ( products = (
Product.objects.annotate(total_qty=Sum("stocks__quantity")) 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] .order_by("total_qty")[:5]
) )
for p in products: 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}" cache_key = f"dashboard_cb:{period_days}"
cached_pack = None cached_pack = cache.get(cache_key, None)
with suppress(Exception):
cached_pack = cache.get(cache_key)
if cached_pack is None: if cached_pack is None:
cached_pack = { cached_pack = {
"kpi": kpi, "kpi": kpi,
@ -725,12 +719,11 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"refund_rate": refund_rate_cur, "refund_rate": refund_rate_cur,
"low_stock_products": low_stock_list, "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: dict[str, str | int | float | None] | None = None
most_popular_list: list[dict[str, str | int | float | None]] = [] most_popular_list: list[dict[str, str | int | float | None]] = []
with suppress(Exception): try:
popular_qs = ( popular_qs = (
OrderProduct.objects.filter( OrderProduct.objects.filter(
status="FINISHED", order__status="FINISHED", product__isnull=False 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), "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] = { customers_mix: dict[str, int | float] = {
"new": 0, "new": 0,
@ -784,7 +779,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"new_pct": 0.0, "new_pct": 0.0,
"returning_pct": 0.0, "returning_pct": 0.0,
} }
with suppress(Exception): try:
mix = get_customer_mix() mix = get_customer_mix()
n = int(mix.get("new", 0)) n = int(mix.get("new", 0))
r = int(mix.get("returning", 0)) r = int(mix.get("returning", 0))
@ -798,6 +793,8 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"returning_pct": ret_pct, "returning_pct": ret_pct,
"total": t, "total": t,
} }
except Exception as exc:
logger.error("Failed to build customer mix: %s", exc)
shipped_vs_digital: dict[str, int | float] = { shipped_vs_digital: dict[str, int | float] = {
"digital_qty": 0, "digital_qty": 0,
@ -807,7 +804,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"digital_pct": 0.0, "digital_pct": 0.0,
"shipped_pct": 0.0, "shipped_pct": 0.0,
} }
with suppress(Exception): try:
svd = get_shipped_vs_digital_mix() svd = get_shipped_vs_digital_mix()
dq = int(svd.get("digital_qty", 0)) dq = int(svd.get("digital_qty", 0))
sq = int(svd.get("shipped_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, "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]] = [] most_returned_products = get_top_returned_products()
with suppress(Exception):
most_returned_products = get_top_returned_products()
top_categories: list[dict[str, str | int | float]] = [] top_categories = get_top_categories_by_qty()
with suppress(Exception):
top_categories = get_top_categories_by_qty()
context.update( context.update(
{ {

View file

@ -12,10 +12,10 @@ upstream storefront_frontend {
server { server {
listen 443 ssl http2; 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 /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/schon.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -72,10 +72,10 @@ server {
server { server {
listen 443 ssl http2; 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 /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/schon.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -128,10 +128,10 @@ server {
server { server {
listen 443 ssl http2; 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 /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/schon.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -173,6 +173,6 @@ server {
server { server {
listen 80; 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; return 301 https://$host$request_uri;
} }

View file

@ -361,10 +361,10 @@ TIME_ZONE: str = getenv("TIME_ZONE", "Europe/London")
WHITENOISE_MANIFEST_STRICT: bool = False 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" 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" MEDIA_ROOT: Path = BASE_DIR / "media"
AUTH_USER_MODEL: str = "vibes_auth.User" AUTH_USER_MODEL: str = "vibes_auth.User"

View file

@ -1,3 +1,5 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -97,3 +99,6 @@ urlpatterns = [
admin.site.urls, admin.site.urls,
), ),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -44,8 +44,8 @@ if [ -f .env ]; then
fi fi
SCHON_PROJECT_NAME=$(prompt_default SCHON_PROJECT_NAME Schon) SCHON_PROJECT_NAME=$(prompt_default SCHON_PROJECT_NAME Schon)
SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.com) SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.fureunoir.com)
SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.com) SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.fureunoir.com)
SENTRY_DSN=$(prompt_default SENTRY_DSN "") SENTRY_DSN=$(prompt_default SENTRY_DSN "")
DEBUG=$(prompt_default DEBUG 1) DEBUG=$(prompt_default DEBUG 1)
TIME_ZONE=$(prompt_default TIME_ZONE "Europe/London") 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) SECRET_KEY=$(prompt_autogen SECRET_KEY 32)
JWT_SIGNING_KEY=$(prompt_autogen JWT_SIGNING_KEY 64) JWT_SIGNING_KEY=$(prompt_autogen JWT_SIGNING_KEY 64)
ALLOWED_HOSTS=$(prompt_default ALLOWED_HOSTS "schon.com api.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.com https://api.schon.com https://www.schon.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") CORS_ALLOWED_ORIGINS=$(prompt_default CORS_ALLOWED_ORIGINS "$CSRF_TRUSTED_ORIGINS")
POSTGRES_DB=$(prompt_default POSTGRES_DB schon) 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) PROMETHEUS_PASSWORD=$(prompt_autogen PROMETHEUS_PASSWORD 16)
EMAIL_BACKEND=$(prompt_default EMAIL_BACKEND django.core.mail.backends.smtp.EmailBackend) 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_PORT=$(prompt_default EMAIL_PORT 465)
EMAIL_USE_TLS=$(prompt_default EMAIL_USE_TLS 0) EMAIL_USE_TLS=$(prompt_default EMAIL_USE_TLS 0)
EMAIL_USE_SSL=$(prompt_default EMAIL_USE_SSL 1) 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_FROM=$EMAIL_HOST_USER
EMAIL_HOST_PASSWORD=$(prompt_default EMAIL_HOST_PASSWORD SUPERSECRETEMAILHOSTPASSWORD) EMAIL_HOST_PASSWORD=$(prompt_default EMAIL_HOST_PASSWORD SUPERSECRETEMAILHOSTPASSWORD)

View file

@ -50,8 +50,8 @@ if (Test-Path '.env')
} }
$SCHON_PROJECT_NAME = Prompt-Default 'SCHON_PROJECT_NAME' 'Schon' $SCHON_PROJECT_NAME = Prompt-Default 'SCHON_PROJECT_NAME' 'Schon'
$SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.com' $SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.fureunoir.com'
$SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.com' $SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.fureunoir.com'
$SENTRY_DSN = Prompt-Default 'SENTRY_DSN' '' $SENTRY_DSN = Prompt-Default 'SENTRY_DSN' ''
$DEBUG = Prompt-Default 'DEBUG' '1' $DEBUG = Prompt-Default 'DEBUG' '1'
$TIME_ZONE = Prompt-Default 'TIME_ZONE' 'Europe/London' $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 $SECRET_KEY = Prompt-AutoGen 'SECRET_KEY' 32
$JWT_SIGNING_KEY = Prompt-AutoGen 'JWT_SIGNING_KEY' 64 $JWT_SIGNING_KEY = Prompt-AutoGen 'JWT_SIGNING_KEY' 64
$ALLOWED_HOSTS = Prompt-Default 'ALLOWED_HOSTS' 'schon.com api.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.com https://api.schon.com https://www.schon.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 $CORS_ALLOWED_ORIGINS = Prompt-Default 'CORS_ALLOWED_ORIGINS' $CSRF_TRUSTED_ORIGINS
$POSTGRES_DB = Prompt-Default 'POSTGRES_DB' 'schon' $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 $PROMETHEUS_PASSWORD = Prompt-AutoGen 'PROMETHEUS_PASSWORD' 16
$EMAIL_BACKEND = Prompt-Default 'EMAIL_BACKEND' 'django.core.mail.backends.smtp.EmailBackend' $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_PORT = Prompt-Default 'EMAIL_PORT' '465'
$EMAIL_USE_TLS = Prompt-Default 'EMAIL_USE_TLS' '0' $EMAIL_USE_TLS = Prompt-Default 'EMAIL_USE_TLS' '0'
$EMAIL_USE_SSL = Prompt-Default 'EMAIL_USE_SSL' '1' $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_FROM = Prompt-Default 'EMAIL_FROM' $EMAIL_HOST_USER
$EMAIL_HOST_PASSWORD = Prompt-Default 'EMAIL_HOST_PASSWORD' 'SUPERSECRETEMAILHOSTPASSWORD' $EMAIL_HOST_PASSWORD = Prompt-Default 'EMAIL_HOST_PASSWORD' 'SUPERSECRETEMAILHOSTPASSWORD'