Fix fulfillment consumables pricing architecture

- Add pricePerUnit field to Supply model for seller pricing
- Fix updateSupplyPrice mutation to update pricePerUnit only
- Separate purchase price (price) from selling price (pricePerUnit)
- Fix GraphQL mutations to include organization field (CREATE/UPDATE_LOGISTICS)
- Update GraphQL types to make Supply.price required again
- Add comprehensive pricing rules to rules-complete.md sections 11.7.5 and 18.8
- Fix supplies-tab.tsx to show debug info and handle user loading

Architecture changes:
• Supply.price = purchase price from supplier (immutable)
• Supply.pricePerUnit = selling price to sellers (mutable by fulfillment)
• Warehouse shows purchase price only (readonly)
• Services shows/edits selling price only

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-07 14:33:40 +03:00
parent cd7dcd9333
commit 0304f69410
6 changed files with 371 additions and 54 deletions

View File

@ -203,7 +203,8 @@ model Supply {
id String @id @default(cuid())
name String
description String?
price Decimal @db.Decimal(10, 2)
price Decimal @db.Decimal(10, 2) // Цена закупки у поставщика (не меняется)
pricePerUnit Decimal? @db.Decimal(10, 2) // Цена продажи селлерам (устанавливается фулфилментом)
quantity Int @default(0)
unit String @default("шт")
category String @default("Расходники")

View File

@ -2282,7 +2282,61 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins);
- С визуальной индикацией состояния (активные/неактивные/без цены)
#### 11.7.5 Технические требования
#### 11.7.5 Разделение цен закупки и продажи
**КРИТИЧЕСКОЕ ПРАВИЛО**: Расходники фулфилмента имеют **ДВЕ РАЗНЫЕ ЦЕНЫ** для разных бизнес-процессов:
1. **ЦЕНА ЗАКУПКИ** (`Supply.price`) - цена, по которой фулфилмент купил расходник у поставщика
2. **ЦЕНА ПРОДАЖИ** (`Supply.pricePerUnit`) - цена, по которой фулфилмент продает расходник селлерам
**ПОЛЯ В БАЗЕ ДАННЫХ**:
```prisma
model Supply {
price Decimal @db.Decimal(10, 2) // Цена закупки у поставщика (НЕИЗМЕННАЯ)
pricePerUnit Decimal? @db.Decimal(10, 2) // Цена продажи селлерам (устанавливается фулфилментом)
}
```
**ПРАВИЛА ОТОБРАЖЕНИЯ ПО РАЗДЕЛАМ**:
**РАЗДЕЛ "СКЛАД → РАСХОДНИКИ ФУЛФИЛМЕНТА"**:
- Показывает `Supply.price` (цена закупки)
- Цена ТОЛЬКО ДЛЯ ЧТЕНИЯ, нельзя изменять
- Отражает историческую стоимость приобретения
**РАЗДЕЛ "УСЛУГИ → РАСХОДНИКИ"**:
- Показывает и редактирует `Supply.pricePerUnit` (цена продажи)
- Единственное место где можно изменить цену для селлеров
- Влияет на рецептуры и расчеты для селлеров
**БИЗНЕС-ЛОГИКА СОЗДАНИЯ**:
ПРИ ПОСТУПЛЕНИИ ОТ ПОСТАВЩИКА:
```typescript
const supply = await prisma.supply.create({
data: {
price: item.price, // Цена поставщика → ЗАФИКСИРОВАНА
pricePerUnit: null, // Цена продажи → ПУСТАЯ
},
})
```
УСТАНОВКА ЦЕНЫ ПРОДАЖИ (в разделе "Услуги"):
```typescript
const updated = await prisma.supply.update({
data: {
pricePerUnit: newPrice, // ТОЛЬКО цена продажи
// price НЕ ТРОГАЕМ - остается цена закупки
},
})
```
#### 11.7.6 Технические требования
**GraphQL типы:**
@ -2743,6 +2797,9 @@ const wholesalePartners = await prisma.counterparty.findMany({
24. ❌ **ИСПОЛЬЗОВАТЬ НАЗВАНИЯ ОРГАНИЗАЦИЙ В ЛОГИКЕ БЕЗОПАСНОСТИ** - проверки доступа только по `organization.type` и системным ID
25. ❌ **СОЗДАВАТЬ УСЛОВИЯ НА ОСНОВЕ ПОЛЬЗОВАТЕЛЬСКИХ СТРОК** - никаких `if (name.includes())` для определения функционала
26. ❌ **ПУТАТЬ ДАННЫЕ И ФУНКЦИОНАЛ** - "ОПТ Маркет" (название рынка) ≠ "Маркет" (раздел системы)
27. ❌ **ПРЕДСТАВЛЯТЬ ИНТЕРПРЕТАЦИИ КАК ФАКТЫ** - всегда четко разделять прямые цитаты из правил и логические выводы
28. ❌ **ОТВЕЧАТЬ БЕЗ ССЫЛОК НА ИСТОЧНИКИ** - при ссылке на правила всегда указывать номер строки или раздел
29. ❌ **ИСПОЛЬЗОВАТЬ КАТЕГОРИЧНЫЕ УТВЕРЖДЕНИЯ БЕЗ ДОКАЗАТЕЛЬСТВ** - избегать "ТОЧНО!", "ИМЕННО ТАК!" без прямых цитат
### 17.2 ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА:
@ -2756,8 +2813,41 @@ const wholesalePartners = await prisma.counterparty.findMany({
8. ✅ Проверка доступности товаров перед заказом
9. ✅ Соблюдение жизненного цикла статусов поставок
10. ✅ Фиксация план/факт в процессе создания продукта
11. ✅ **УКАЗЫВАТЬ ИСТОЧНИКИ ИНФОРМАЦИИ** - при ссылке на правила обязательно указывать строку/раздел
12. ✅ **РАЗДЕЛЯТЬ ФАКТЫ И ИНТЕРПРЕТАЦИИ** - четко маркировать что взято из правил, а что является выводом
13. ✅ **ИСПОЛЬЗОВАТЬ ОСТОРОЖНЫЕ ФОРМУЛИРОВКИ** - "согласно правилам", "возможно", "требует уточнения"
### 17.3 🔒 ПРАВИЛА БЕЗОПАСНОСТИ: Разделение данных и функционала
### 17.3 📝 ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ С ФАКТАМИ
**При ссылке на правила ОБЯЗАТЕЛЬНО использовать формат:**
✅ **ПРАВИЛЬНО:**
```
📖 ФАКТ из rules-complete.md (строка 2225): "установка цены за единицу"
🧠 МОЯ ИНТЕРПРЕТАЦИЯ: возможно, это происходит в разделе X
❓ ПРЕДПОЛОЖЕНИЕ: требует уточнения у пользователя
⚠️ НЕ НАЙДЕНО в правилах: информация о точном местоположении
```
❌ **НЕПРАВИЛЬНО:**
```
"Да! Точно понимаю! Фулфилмент устанавливает цены в разделе X!"
```
**ОБЯЗАТЕЛЬНАЯ МАРКИРОВКА:**
- 📖 **ФАКТ** - прямая цитата из правил с номером строки
- 🧠 **ИНТЕРПРЕТАЦИЯ** - мой логический вывод (четко обозначен)
- ❓ **ПРЕДПОЛОЖЕНИЕ** - гипотеза, требующая подтверждения
- ⚠️ **НЕ НАЙДЕНО** - информация отсутствует в правилах
**СТОП-СЛОВА (избегать без доказательств):**
❌ "ТОЧНО!", "ИМЕННО ТАК!", "ДА! ПОНИМАЮ!", "АБСОЛЮТНО ВЕРНО!"
✅ "Согласно правилам...", "Не указано, но возможно...", "Требует уточнения"
### 17.4 🔒 ПРАВИЛА БЕЗОПАСНОСТИ: Разделение данных и функционала
#### КРИТИЧЕСКОЕ ПРАВИЛО БЕЗОПАСНОСТИ
@ -2817,6 +2907,131 @@ if (ALLOWED_FULFILLMENT_IDS.includes(organization.id)) {
**ПРАВИЛО**: Физический рынок "ОПТ Маркет" - это просто строка данных. Раздел "Маркет" (/market) - это системный функционал. Они никак не связаны и не должны влиять друг на друга.
### 17.5 📦 УПРАВЛЕНИЕ СВЯЗЯМИ ТОВАР-КАРТОЧКА В РЕЦЕПТУРЕ
#### 17.5.1 Общие принципы
**НАЗНАЧЕНИЕ**: Связь товара с карточкой маркетплейса - это метаданные для учета, НЕ влияющие на физический состав продукта.
**ФОРМУЛА ПРОДУКТА НЕИЗМЕННА**:
```
ПРОДУКТ = Товар + Услуга(и) + Расходники селлера + Расходники ФФ
```
**СВЯЗЬ С МП** = отдельные метаданные для логистики и учета
#### 17.5.2 UI компонент связи с карточками
**РАСПОЛОЖЕНИЕ**: В форме создания поставки, в секции каждого товара
**ТИП КОМПОНЕНТА**: Dropdown с поиском и фильтрацией
**ИСТОЧНИК ДАННЫХ**: База данных карточек маркетплейсов селлера (GraphQL запрос)
#### 17.5.3 Логика состояний карточек
**✅ СВЯЗАНО** - карточка уже привязана к этому товару:
- Показывать зеленую галочку
- Текст: "Название карточки - Связано"
- Можно отвязать (сброс в "Без привязки")
**⚠️ ДОСТУПНО** - карточка свободна для привязки:
- Показывать желтый значок предупреждения
- Текст: "Название карточки - Доступно"
- Можно привязать к текущему товару
**❌ ЗАНЯТО** - карточка привязана к другому товару:
- Показывать красный крестик
- Текст: "Название карточки - Занято (товар: 'Название')"
- Пункт заблокирован (disabled)
- Показывать для информации, но нельзя выбрать
**🔍 БЕЗ ПРИВЯЗКИ** - товар не связан с карточкой:
- Пункт по умолчанию
- Показывать серый значок
- Текст: "Без привязки к карточке"
#### 17.5.4 Техническая реализация
**GraphQL запрос**:
```graphql
query GetSellerCards {
myMarketplaceCards {
id
title
marketplace
article
linkedProductId # null если свободна
linkedProduct {
# для отображения занятости
id
name
}
}
}
```
**Логика фильтрации**:
- Все карточки селлера показываются в dropdown
- Статус определяется по полю `linkedProductId`
- Автосвязка: карточки с похожим названием показываются первыми
**Сохранение**:
- При создании поставки связь сохраняется в поле `marketplaceCardId` рецептуры
- При изменении связи обновляется поле `linkedProductId` в карточке
#### 17.5.5 UX поведение
**ПОИСК В DROPDOWN**:
- Фильтрация по названию карточки
- Фильтрация по артикулу маркетплейса
- Автофокус при открытии
**ГРУППИРОВКА**:
```
[Dropdown: Выберите карточку Wildberries ▼]
├─ 🔍 БЕЗ ПРИВЯЗКИ
├─ ────── ДОСТУПНЫЕ ──────
├─ ⚠️ "Кроссовки Nike Air" - Доступно
├─ ⚠️ "Футболка Adidas" - Доступно
├─ ────── СВЯЗАННЫЕ ──────
├─ ✅ "Джинсы Levi's" - Связано
├─ ────── ЗАНЯТЫЕ ──────
└─ ❌ "Куртка Puma" - Занято (товар "Верхняя одежда") [disabled]
```
**ВАЛИДАЦИЯ**:
- Связь опциональна - можно создать поставку без привязки
- При выборе занятой карточки показывать предупреждение
- При отвязке подтверждать действие
#### 17.5.6 Интеграция с существующими правилами
**СОВМЕСТИМОСТЬ**:
- Не нарушает существующую логику создания поставок
- Дополняет рецептуру метаданными
- Совместима с типами поставок (карточки/поставщики)
**ОБЯЗАТЕЛЬНОСТЬ**:
- Связь с карточкой - ОПЦИОНАЛЬНА
- Товар может существовать без привязки к МП
- Карточка может существовать без привязки к товару
**ПРИОРИТЕТ РАЗРАБОТКИ**: Средний (не блокирует основную функциональность)
---
## 18. 🛠️ GRAPHQL И TYPESCRIPT ПРАВИЛА
@ -2966,6 +3181,70 @@ query GetMarketProducts {
- **Валидация обязательных параметров** на уровне схемы (`organizationId: ID!`)
- **Кеширование обходить при проблемах** через `fetchPolicy: 'network-only'`
### 18.8 GraphQL правила для поля organization в мутациях
#### 18.8.1 Обязательность поля organization
**ПРАВИЛО**: Все мутации, возвращающие объекты с типом, включающим `organization: Organization!`, ДОЛЖНЫ запрашивать это поле.
**ПРОБЛЕМА**: Apollo Client кэш ожидает поле `organization` в ответе, если оно определено в GraphQL типе как обязательное.
#### 18.8.2 Правильное написание мутаций
**❌ НЕПРАВИЛЬНО** (вызывает ошибку Apollo Client):
```graphql
mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) {
updateLogistics(id: $id, input: $input) {
success
logistics {
id
fromLocation
# НЕТ поля organization - ОШИБКА кэша!
}
}
}
```
**✅ ПРАВИЛЬНО** (работает корректно):
```graphql
mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) {
updateLogistics(id: $id, input: $input) {
success
logistics {
id
fromLocation
organization {
# ОБЯЗАТЕЛЬНО включить!
id
name
fullName
}
}
}
}
```
#### 18.8.3 Чек-лист для мутаций
**ОБЯЗАТЕЛЬНАЯ ПРОВЕРКА** перед созданием мутации:
1. ✅ Проверить GraphQL тип возвращаемого объекта
2. ✅ Если есть поле `organization: Organization!` - добавить в запрос
3. ✅ Включить минимальные поля: `id`, `name`, `fullName`
4. ✅ Проверить resolver включает `include: { organization: true }`
**ПРИМЕНЯЕТСЯ К**:
- `CREATE_LOGISTICS` ✅ Исправлено
- `UPDATE_LOGISTICS` ✅ Исправлено
- `CREATE_SERVICE` - проверить при разработке
- `UPDATE_SERVICE` - проверить при разработке
- Все другие мутации с организационными объектами
**ОШИБКА БЕЗ ПОЛЯ**: `Error converting field "organization" of expected non-nullable type`
---
## 19. 🔧 АРХИТЕКТУРНЫЕ ПРИНЦИПЫ
@ -3353,7 +3632,7 @@ const handleSuppliesClick = () => {
_Эта база знаний создана путем объединения rules-unified.md (v3.0) и fulfillment-cabinet-rules.md (v1.0) с устранением всех несоответствий и добавлением критически важных улучшений: быстрый справочник, глоссарий терминов, детальные алгоритмы процессов, edge cases._
_Версия: 10.0_
_Версия: 10.1_
ата создания: 2025_
_Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗРАБОТКЕ_
@ -3441,3 +3720,11 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
- **РАСШИРЕН ГЛОССАРИЙ**: Контекстно-зависимые термины для SupplyOrder
- **УТОЧНЕНИЕ ТЕРМИНОВ**: Четкое разделение "Маркет" (раздел) vs "Маркетплейс" (внешние площадки)
- **ПРИМЕРЫ УЯЗВИМОСТЕЙ**: Конкретные примеры безопасного и небезопасного кода
### 📝 КАЧЕСТВО ОТВЕТОВ v10.1:
- **НОВЫЕ ЗАПРЕТЫ 27-29**: Запрет представления интерпретаций как фактов
- **ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ 17.3**: Четкое разделение фактов, интерпретаций и предположений
- **СИСТЕМА МАРКИРОВКИ**: 📖 ФАКТ, 🧠 ИНТЕРПРЕТАЦИЯ, ПРЕДПОЛОЖЕНИЕ, НЕ НАЙДЕНО
- **СТОП-СЛОВА**: Список категоричных утверждений для избегания без доказательств
- **ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА 11-13**: Указание источников и осторожные формулировки

View File

@ -54,12 +54,23 @@ export function SuppliesTab() {
const [isSaving, setIsSaving] = useState(false)
const [isInitialized, setIsInitialized] = useState(false)
// Debug информация
console.log('SuppliesTab - User:', user?.phone, 'Type:', user?.organization?.type)
// GraphQL запросы и мутации
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
skip: user?.organization?.type !== 'FULFILLMENT',
skip: !user || user?.organization?.type !== 'FULFILLMENT',
})
const [updateSupplyPrice] = useMutation(UPDATE_SUPPLY_PRICE)
// Debug GraphQL запроса
console.log('SuppliesTab - Query:', {
skip: !user || user?.organization?.type !== 'FULFILLMENT',
loading,
error: error?.message,
dataLength: data?.mySupplies?.length,
})
const supplies = data?.mySupplies || []
// Преобразуем загруженные расходники в редактируемый формат
@ -130,7 +141,7 @@ export function SuppliesTab() {
if (field !== 'pricePerUnit') {
return // Только цену можно редактировать
}
setEditableSupplies((prev) =>
prev.map((supply) => {
if (supply.id !== supplyId) return supply
@ -155,7 +166,7 @@ export function SuppliesTab() {
for (const supply of suppliesToSave) {
// Проверяем валидность цены (может быть пустой)
const pricePerUnit = supply.pricePerUnit.trim() ? parseFloat(supply.pricePerUnit) : null
if (supply.pricePerUnit.trim() && (isNaN(pricePerUnit!) || pricePerUnit! <= 0)) {
toast.error('Введите корректную цену')
setIsSaving(false)
@ -187,9 +198,7 @@ export function SuppliesTab() {
}
// Сбрасываем флаги изменений
setEditableSupplies((prev) =>
prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })),
)
setEditableSupplies((prev) => prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })))
toast.success('Цены успешно обновлены')
} catch (error) {
@ -212,7 +221,9 @@ export function SuppliesTab() {
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-white mb-1">Расходники со склада</h2>
<p className="text-white/70 text-sm">Расходники появляются автоматически из поставок. Можно только установить цену.</p>
<p className="text-white/70 text-sm">
Расходники появляются автоматически из поставок. Можно только установить цену.
</p>
</div>
<div className="flex gap-3">
@ -261,7 +272,19 @@ export function SuppliesTab() {
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">Ошибка загрузки</h3>
<p className="text-white/70 text-sm mb-4">Не удалось загрузить расходники</p>
<p className="text-white/70 text-sm mb-4">
Не удалось загрузить расходники
{process.env.NODE_ENV === 'development' && (
<>
<br />
<span className="text-xs text-red-300">
Debug: {error.message}
<br />
User type: {user?.organization?.type}
</span>
</>
)}
</p>
<Button
onClick={() => refetch()}
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white"
@ -301,9 +324,7 @@ export function SuppliesTab() {
key={supply.id || index}
className={`border-t border-white/10 hover:bg-white/5 ${
supply.hasChanges ? 'bg-blue-500/10' : ''
} ${
supply.isAvailable ? '' : 'opacity-60'
}`}
} ${supply.isAvailable ? '' : 'opacity-60'}`}
>
<td className="p-4 text-white/80">{index + 1}</td>
@ -346,13 +367,13 @@ export function SuppliesTab() {
<td className="p-4">
<span className="text-white font-medium">{supply.name}</span>
</td>
{/* Остаток на складе */}
<td className="p-4">
<div className="flex items-center gap-2">
<span className={`text-sm font-medium ${
supply.isAvailable ? 'text-green-400' : 'text-red-400'
}`}>
<span
className={`text-sm font-medium ${supply.isAvailable ? 'text-green-400' : 'text-red-400'}`}
>
{supply.warehouseStock}
</span>
{!supply.isAvailable && (
@ -362,7 +383,7 @@ export function SuppliesTab() {
)}
</div>
</td>
{/* Единица измерения */}
<td className="p-4">
<span className="text-white/80">{supply.unit}</span>
@ -382,7 +403,9 @@ export function SuppliesTab() {
/>
) : (
<span className="text-white/80">
{supply.pricePerUnit ? `${parseFloat(supply.pricePerUnit).toLocaleString()}` : 'Не установлена'}
{supply.pricePerUnit
? `${parseFloat(supply.pricePerUnit).toLocaleString()}`
: 'Не установлена'}
</span>
)}
</td>

View File

@ -650,7 +650,6 @@ export const UPDATE_SUPPLY_PRICE = gql`
}
`
// Мутация для заказа поставки расходников
export const CREATE_SUPPLY_ORDER = gql`
mutation CreateSupplyOrder($input: SupplyOrderInput!) {
@ -746,6 +745,11 @@ export const CREATE_LOGISTICS = gql`
description
createdAt
updatedAt
organization {
id
name
fullName
}
}
}
}
@ -765,6 +769,11 @@ export const UPDATE_LOGISTICS = gql`
description
createdAt
updatedAt
organization {
id
name
fullName
}
}
}
}

View File

@ -718,7 +718,7 @@ export const resolvers = {
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
organizationId: currentUser.organization.id,
suppliesCount: transformedSupplies.length,
supplies: transformedSupplies.map(s => ({
supplies: transformedSupplies.map((s) => ({
id: s.id,
name: s.name,
pricePerUnit: s.pricePerUnit,
@ -765,7 +765,7 @@ export const resolvers = {
// Расходники фулфилмента из склада (новая архитектура - синхронизация со склада)
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
if (!context.user) {
console.warn('❌ No user in context')
throw new GraphQLError('Требуется авторизация', {
@ -826,7 +826,7 @@ export const resolvers = {
})
// Преобразуем в формат для фронтенда
return supplies.map(supply => ({
return supplies.map((supply) => ({
...supply,
price: supply.price ? parseFloat(supply.price.toString()) : 0,
shippedQuantity: 0, // Добавляем для совместимости
@ -1387,7 +1387,6 @@ export const resolvers = {
// Мои товары и расходники (для поставщиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
@ -1399,7 +1398,6 @@ export const resolvers = {
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
@ -3607,7 +3605,6 @@ export const resolvers = {
}
},
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
updateSupplyPrice: async (
_: unknown,
@ -3655,7 +3652,7 @@ export const resolvers = {
const updatedSupply = await prisma.supply.update({
where: { id: args.id },
data: {
price: args.input.pricePerUnit, // Обновляем только цену
pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки
updatedAt: new Date(),
},
include: { organization: true },
@ -4050,7 +4047,7 @@ export const resolvers = {
return {
name: product.name,
description: product.description || `Заказано у ${partner.name}`,
price: product.price,
price: product.price, // Цена закупки у поставщика
quantity: item.quantity,
unit: 'шт',
category: productWithCategory?.category?.name || 'Расходники',
@ -5830,7 +5827,7 @@ export const resolvers = {
data: {
name: item.product.name,
description: item.product.description || `Поставка от ${existingOrder.partner.name}`,
price: item.price,
price: item.price, // Цена закупки у поставщика
quantity: item.quantity,
unit: 'шт',
category: item.product.category?.name || 'Расходники',
@ -6590,7 +6587,7 @@ export const resolvers = {
description: isSellerSupply
? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}`
: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
price: item.price,
price: item.price, // Цена закупки у поставщика
quantity: item.quantity,
currentStock: item.quantity,
usedStock: 0,

View File

@ -36,7 +36,7 @@ export const typeDefs = gql`
# Расходники селлеров (материалы клиентов)
mySupplies: [Supply!]!
# Доступные расходники для рецептур селлеров (только с ценой и в наличии)
getAvailableSuppliesForRecipe: [SupplyForRecipe!]!
@ -522,25 +522,25 @@ export const typeDefs = gql`
name: String!
description: String
# Новые поля для Services архитектуры
pricePerUnit: Float # Цена за единицу для рецептур (может быть null)
unit: String! # Единица измерения: "шт", "кг", "м"
warehouseStock: Int! # Остаток на складе (readonly)
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
pricePerUnit: Float # Цена за единицу для рецептур (может быть null)
unit: String! # Единица измерения: "шт", "кг", "м"
warehouseStock: Int! # Остаток на складе (readonly)
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
warehouseConsumableId: ID! # Связь со складом
# Поля из базы данных для обратной совместимости
price: Float! # Из Prisma schema
quantity: Int! # Из Prisma schema
category: String! # Из Prisma schema
status: String! # Из Prisma schema
date: DateTime! # Из Prisma schema
supplier: String! # Из Prisma schema
minStock: Int! # Из Prisma schema
currentStock: Int! # Из Prisma schema
usedStock: Int! # Из Prisma schema
type: String! # Из Prisma schema (SupplyType enum)
sellerOwnerId: ID # Из Prisma schema
sellerOwner: Organization # Из Prisma schema
shopLocation: String # Из Prisma schema
price: Float! # Цена закупки у поставщика (не меняется)
quantity: Int! # Из Prisma schema
category: String! # Из Prisma schema
status: String! # Из Prisma schema
date: DateTime! # Из Prisma schema
supplier: String! # Из Prisma schema
minStock: Int! # Из Prisma schema
currentStock: Int! # Из Prisma schema
usedStock: Int! # Из Prisma schema
type: String! # Из Prisma schema (SupplyType enum)
sellerOwnerId: ID # Из Prisma schema
sellerOwner: Organization # Из Prisma schema
shopLocation: String # Из Prisma schema
imageUrl: String
createdAt: DateTime!
updatedAt: DateTime!
@ -551,15 +551,15 @@ export const typeDefs = gql`
type SupplyForRecipe {
id: ID!
name: String!
pricePerUnit: Float! # Всегда не null
pricePerUnit: Float! # Всегда не null
unit: String!
imageUrl: String
warehouseStock: Int! # Всегда > 0
warehouseStock: Int! # Всегда > 0
}
# Для обновления цены расходника в разделе Услуги
input UpdateSupplyPriceInput {
pricePerUnit: Float # Может быть null (цена не установлена)
pricePerUnit: Float # Может быть null (цена не установлена)
}
input UseFulfillmentSuppliesInput {
@ -567,7 +567,7 @@ export const typeDefs = gql`
quantityUsed: Int!
description: String # Описание использования (например, "Подготовка 300 продуктов")
}
# Устаревшие типы для обратной совместимости
input SupplyInput {
name: String!