feat(fixtures): add demo.json for gemstone and jewelry showcase
Includes categories, brands, attributes, products, tags, and vendor/demo user data for demonstration purposes.
710
engine/core/fixtures/demo.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
engine/core/fixtures/demo_products_images/AME-SIB-850-DP.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
engine/core/fixtures/demo_products_images/AME-TRI-680-BC.jpg
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
engine/core/fixtures/demo_products_images/AME-URU-1230.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
engine/core/fixtures/demo_products_images/AQU-SM-420.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-CU-300-G-VS1.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-EM-180-D-VVS2.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-FY-250.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-OV-120-F-IF.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-PC-200-E-VS2.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-PNK-050-FL.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
engine/core/fixtures/demo_products_images/DIA-RB-150-D-VVS1.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
engine/core/fixtures/demo_products_images/EME-BRA-120-MG.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
engine/core/fixtures/demo_products_images/EME-COL-280-MZ.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
engine/core/fixtures/demo_products_images/EME-ZAM-350-VG.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
engine/core/fixtures/demo_products_images/OPL-BLD-850.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
engine/core/fixtures/demo_products_images/OPL-BLK-520-LR.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
engine/core/fixtures/demo_products_images/OPL-ETH-380-WL.jpg
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
engine/core/fixtures/demo_products_images/OPL-FIRE-210-MX.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
engine/core/fixtures/demo_products_images/PRL-AKO-7MM-STR.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
engine/core/fixtures/demo_products_images/PRL-FW-SET-10.jpg
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
engine/core/fixtures/demo_products_images/PRL-SSG-14MM.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
engine/core/fixtures/demo_products_images/RUB-BUR-250-PB.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
engine/core/fixtures/demo_products_images/RUB-MOZ-180-VR.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
engine/core/fixtures/demo_products_images/RUB-STAR-320-SR.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
engine/core/fixtures/demo_products_images/SAP-CEY-220-RB.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
engine/core/fixtures/demo_products_images/SAP-KAS-300-CB.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
engine/core/fixtures/demo_products_images/SAP-PNK-180-HP.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
engine/core/fixtures/demo_products_images/SAP-YEL-400-GD.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
engine/core/fixtures/demo_products_images/TAN-AAA-580-VB.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
engine/core/fixtures/demo_products_images/TAN-PAIR-200.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
engine/core/fixtures/demo_products_images/TOU-IND-360.jpg
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
engine/core/fixtures/demo_products_images/TOU-PAR-120-NB.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
engine/core/fixtures/demo_products_images/TOU-WM-850.jpg
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
engine/core/fixtures/demo_products_images/placeholder.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
|
|
@ -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",)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||